更新 renren.js (#142)
This commit is contained in:
1 parent
1148464d8c
commit
a04c9dfaf4
1 file changed
+119
-77
+119
-77
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user