更新 renren.js (#142)

This commit is contained in:
Wo254992 authored and GitHub committed 2026-01-11 23:24:31 +08:00
1 parent 1148464d8c
commit a04c9dfaf4
1 file changed
+119 -77
+119 -77
View File
@@ -17,20 +17,31 @@ export default class RenrenSource extends BaseSource {
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'
APP_VERSION: "10.31.2",
USER_AGENT: 'Mozilla/5.0 (Linux; Android 16; 23127PN0CC Build/BP2A.250605.031.A3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/143.0.7499.146 Mobile Safari/537.36 App/RRSPApp platform/android AppVersion/10.31.2'
};
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',
'deviceId': 'T2%2Bjh%2FnHhJkWEzPnQT2E0%2FEw865FTT0uL%2BiBwRa2ZdM%3D',
'aliId': 'aUzmLtnZIYoDAA9KyLdcLQpM',
'umId': '53e0f078fa8474ae7ba412f766989b54od',
'clientType': 'android_rrsp_xb_XiaoMi',
't': timestamp.toString(),
'sign': sign,
'isAgree': '1',
'cv': '10.27.4'
'cv': this.API_CONFIG.APP_VERSION,
'ct': 'android_rrsp_xb_XiaoMi',
'pkt': 'rrmj',
'p': 'Android',
'wcode': '3',
'et': '2',
'uet': '1',
'folding-screen': '1',
'Accept': 'application/json',
'Accept-Encoding': 'gzip',
'Connection': 'close'
};
if (xCaSign) {
@@ -54,23 +65,30 @@ export default class RenrenSource extends BaseSource {
};
const sign = generateSign(path, timestamp, queryParams, this.API_CONFIG.SECRET_KEY);
const queryString = Object.entries(queryParams)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.map(([k, v]) => `${k}=${encodeURIComponent(v === null || v === undefined ? "" : String(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;
headers['Origin'] = 'https://d.rrsp.com.cn';
headers['Referer'] = 'https://d.rrsp.com.cn/';
const resp = await httpGet(`https://${this.API_CONFIG.SEARCH_HOST}${path}?${queryString}`, {
headers: headers,
retries: 1,
});
if (!resp.data) return [];
// 服务端明确提示"版本过低/强制更新"时:直接返回空,让上层走备用搜索
if (resp?.data?.code === "0001") return [];
const list = resp?.data?.data?.searchDramaList || [];
return list.map((item, idx) => ({
return list.map((item) => ({
provider: "renren",
mediaId: String(item.id),
title: String(item.title || "").replace(/<[^>]+>/g, "").replace(/:/g, ":"),
@@ -82,7 +100,15 @@ export default class RenrenSource extends BaseSource {
currentEpisodeIndex: null,
}));
} catch (error) {
log("error", "getRenrenAppAnimes error:", {
const msg = String(error?.message || "");
const is418 = /status:\s*418\b/.test(msg);
if (is418) {
log("warn", "[Renren] /search/content 被服务端拦截 (418),已降级为备用搜索接口");
return [];
}
log("error", "[Renren] searchAppContent error:", {
message: error.message,
name: error.name,
stack: error.stack,
@@ -118,14 +144,15 @@ export default class RenrenSource extends BaseSource {
headers['ignore'] = 'false';
const resp = await httpGet(`https://${this.API_CONFIG.DRAMA_HOST}${path}?${queryString}`, {
headers: headers
headers: headers,
retries: 1,
});
if (!resp.data) return null;
return resp.data;
} catch (error) {
log("error", "getRenrenAppDramaDetail error:", {
log("error", "[Renren] getAppDramaDetail error:", {
message: error.message,
name: error.name,
stack: error.stack,
@@ -155,7 +182,7 @@ export default class RenrenSource extends BaseSource {
return resp.data;
} catch (error) {
log("error", "getRenrenDramaDetail error:", {
log("error", "[Renren] getAppDanmu error:", {
message: error.message,
name: error.name,
stack: error.stack,
@@ -166,11 +193,14 @@ export default class RenrenSource extends BaseSource {
parseRRSPPFields(pField) {
const parts = String(pField).split(",");
const num = (i, cast, dft) => { try { return cast(parts[i]); } catch { return dft; } };
const num = (i, cast, dft) => {
try { return cast(parts[i]); }
catch { return dft; }
};
const timestamp = num(0, parseFloat, 0);
const mode = num(1, x=>parseInt(x,10),1);
const size = num(2, x=>parseInt(x,10),25);
const color = num(3, x=>parseInt(x,10),16777215);
const mode = num(1, x => parseInt(x, 10), 1);
const size = num(2, x => parseInt(x, 10), 25);
const color = num(3, x => parseInt(x, 10), 16777215);
const userId = parts[6] || "";
const contentId = parts[7] || `${timestamp}:${userId}`;
return { timestamp, mode, size, color, userId, contentId };
@@ -217,7 +247,7 @@ export default class RenrenSource extends BaseSource {
}
async renrenHttpGet(url, { params = {}, headers = {} } = {}) {
const u = updateQueryString(url, params)
const u = updateQueryString(url, params);
const resp = await httpGet(u, {
headers: headers,
retries: 1,
@@ -234,35 +264,33 @@ export default class RenrenSource extends BaseSource {
const headers = this.buildSignedHeaders({ method, url, params, deviceId });
const resp = await httpGet(url + "?" + sortedQueryString(params), {
headers: headers,
retries: 1,
});
return resp;
}
async performNetworkSearch(
keyword,
{
lockRef = null,
lastRequestTimeRef = { value: 0 }, // 调用方传引用
minInterval = 500 // 默认节流间隔(毫秒)
} = {}
) {
async performNetworkSearch(keyword, { lockRef = null, lastRequestTimeRef = { value: 0 }, minInterval = 500 } = {}) {
try {
const url = `https://api.rrmj.plus/m-station/search/drama`;
const params = { keywords: keyword, size: 20, order: "match", search_after: "", isExecuteVipActivity: true };
const params = {
keywords: keyword,
size: 20,
order: "match",
search_after: "",
isExecuteVipActivity: true
};
// 🔒 锁逻辑(可选)
if (lockRef) {
while (lockRef.value) await new Promise(r => setTimeout(r, 50));
lockRef.value = true;
}
// ⏱️ 节流逻辑(依赖 lastRequestTimeRef)
const now = Date.now();
const dt = now - lastRequestTimeRef.value;
if (dt < minInterval) await new Promise(r => setTimeout(r, minInterval - dt));
const resp = await this.renrenRequest("GET", url, params);
lastRequestTimeRef.value = Date.now(); // 更新引用
lastRequestTimeRef.value = Date.now();
if (lockRef) lockRef.value = false;
@@ -270,7 +298,7 @@ export default class RenrenSource extends BaseSource {
const decoded = autoDecode(resp.data);
const list = decoded?.data?.searchDramaList || [];
return list.map((item, idx) => ({
return list.map((item) => ({
provider: "renren",
mediaId: String(item.id),
title: String(item.title || "").replace(/<[^>]+>/g, "").replace(/:/g, ":"),
@@ -282,7 +310,7 @@ export default class RenrenSource extends BaseSource {
currentEpisodeIndex: null,
}));
} catch (error) {
log("error", "getRenrenAnimes error:", {
log("error", "[Renren] performNetworkSearch error:", {
message: error.message,
name: error.name,
stack: error.stack,
@@ -292,37 +320,47 @@ export default class RenrenSource extends BaseSource {
}
async search(keyword) {
const parsedKeyword = { title: keyword, season: null }; // 简化 parse_search_keyword
const parsedKeyword = { title: keyword, season: null };
const searchTitle = parsedKeyword.title;
const searchSeason = parsedKeyword.season;
let allResults = [];
// 先使用APP API进行搜索
// 优先使用 APP 接口搜索
allResults = await this.searchAppContent(searchTitle);
// APP 接口失败时降级到网页接口
if (allResults.length === 0) {
log("info", "[Renren] APP 搜索无结果,降级到网页接口");
const lock = { value: false };
const lastRequestTime = { value: 0 };
allResults = await this.performNetworkSearch(searchTitle, { lockRef: lock, lastRequestTimeRef: lastRequestTime, minInterval: 400 });
allResults = await this.performNetworkSearch(searchTitle, {
lockRef: lock,
lastRequestTimeRef: lastRequestTime,
minInterval: 400
});
}
if (searchSeason == null) return allResults;
// 按 season 过滤
return allResults.filter(r => r.season === searchSeason);
}
async getDetail(id) {
// 优先使用 APP 接口
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 {
if (resp) {
return resp.data;
}
// APP 接口失败时降级到网页接口
log("info", "[Renren] APP 详情接口失败,降级到网页接口");
const url = `https://api.rrmj.plus/m-station/drama/page`;
const params = { hsdrOpen: 0, isAgeLimit: 0, dramaId: String(id), hevcOpen: 1 };
const fallbackResp = await this.renrenRequest("GET", url, params);
if (!fallbackResp.data) return null;
const decoded = autoDecode(fallbackResp.data);
return decoded?.data || null;
}
async getEpisodes(id) {
@@ -330,14 +368,14 @@ export default class RenrenSource extends BaseSource {
if (!detail || !detail.episodeList) return [];
let episodes = [];
detail.episodeList.forEach((ep, idx)=>{
detail.episodeList.forEach((ep, idx) => {
const sid = String(ep.sid || "").trim();
if(!sid) return;
const title = String(ep.title || `第${idx+1}`.padStart(2,"0")+"集");
episodes.push({ sid, order: idx+1, title });
if (!sid) return;
const title = String(ep.title || `第${String(idx + 1).padStart(2, "0")}集`);
episodes.push({ sid, order: idx + 1, title });
});
return episodes.map(e=>({
return episodes.map(e => ({
provider: "renren",
episodeId: e.sid,
title: e.title,
@@ -349,14 +387,12 @@ export default class RenrenSource extends BaseSource {
async handleAnimes(sourceAnimes, queryTitle, curAnimes) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
if (!sourceAnimes || !Array.isArray(sourceAnimes)) {
log("error", "[Renren] sourceAnimes is not a valid array");
return [];
}
// 使用 map 和 async 时需要返回 Promise 数组,并等待所有 Promise 完成
const processRenrenAnimes = await Promise.all(sourceAnimes
await Promise.all(sourceAnimes
.filter(s => titleMatches(s.title, queryTitle))
.map(async (anime) => {
try {
@@ -386,10 +422,11 @@ export default class RenrenSource extends BaseSource {
};
tmpAnimes.push(transformedAnime);
addAnime({ ...transformedAnime, links: links });
addAnime({...transformedAnime, links: links});
if (globals.animes.length > globals.MAX_ANIMES) removeEarliestAnime();
if (globals.animes.length > globals.MAX_ANIMES) {
removeEarliestAnime();
}
}
} catch (error) {
log("error", `[Renren] Error processing anime: ${error.message}`);
@@ -399,37 +436,42 @@ export default class RenrenSource extends BaseSource {
this.sortAndPushAnimesByYear(tmpAnimes, curAnimes);
return processRenrenAnimes;
return tmpAnimes;
}
async getEpisodeDanmu(id) {
// 优先使用 APP 接口
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 {
if (resp) {
return resp;
}
// APP 接口失败时降级到网页接口
log("info", "[Renren] APP 弹幕接口失败,降级到网页接口");
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 fallbackResp = await this.renrenHttpGet(url, { headers });
if (!fallbackResp.data) return null;
const data = autoDecode(fallbackResp.data);
if (Array.isArray(data)) return data;
if (data?.data && Array.isArray(data.data)) return data.data;
return null;
}
async getEpisodeDanmuSegments(id) {
log("info", "获取人人视频弹幕分段列表...", id);
log("info", "[Renren] 获取弹幕分段列表:", id);
return new SegmentListResponse({
"type": "renren",