mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-09-01 14:57:14 +08:00
fix(route): Fix xiaoheihe (#14764)
This commit is contained in:
@@ -520,12 +520,6 @@ router.get('/gov/guangdong/eea/:caty', lazyloadRouteHandler('./routes/gov/guangd
|
||||
|
||||
// 日本国外務省記者会見
|
||||
router.get('/go.jp/mofa', lazyloadRouteHandler('./routes/go.jp/mofa/main'));
|
||||
|
||||
// 小黑盒
|
||||
router.get('/xiaoheihe/user/:id', lazyloadRouteHandler('./routes/xiaoheihe/user'));
|
||||
router.get('/xiaoheihe/news', lazyloadRouteHandler('./routes/xiaoheihe/news'));
|
||||
router.get('/xiaoheihe/discount/:platform?', lazyloadRouteHandler('./routes/xiaoheihe/discount'));
|
||||
|
||||
// ebb
|
||||
router.get('/ebb', lazyloadRouteHandler('./routes/ebb'));
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
const got = require('@/utils/got');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const platform = ctx.params.platform || 'pc';
|
||||
|
||||
const response = await got({
|
||||
method: 'get',
|
||||
url: `https://api.xiaoheihe.cn/game/get_game_list_v3?sort_type=discount&filter_head=${platform}&offset=0&limit=30&os_type=web`,
|
||||
});
|
||||
const data = response.data.result.games;
|
||||
|
||||
ctx.state.data = {
|
||||
title: `小黑盒${platform.toUpperCase()}游戏折扣`,
|
||||
link: 'https://xiaoheihe.cn/games/index',
|
||||
item: data.map((item) => {
|
||||
const title = `${item.name}${item.name_en ? '/' + item.name_en : ''}`;
|
||||
let description = `
|
||||
<img src="${item.image}"/> <br/>
|
||||
`;
|
||||
if (item.platform_infos) {
|
||||
for (const platform of item.platform_infos) {
|
||||
if (platform.price) {
|
||||
if (platform.key) {
|
||||
description += `平台: ${platform.key}<br/>>`;
|
||||
}
|
||||
if (platform.price.current) {
|
||||
description += `当前价格: ${platform.price.current}${platform.price.discount === platform.price.lowest_discount ? '[史低]' : ''}<br/>`;
|
||||
}
|
||||
if (platform.price.initial) {
|
||||
description += `原价: ${platform.price.initial}<br/>`;
|
||||
}
|
||||
if (platform.price.discount_desc) {
|
||||
description += `折扣力度: ${platform.price.discount_desc}<br/>`;
|
||||
}
|
||||
if (platform.price.deadline_date) {
|
||||
description += `截止时间: ${platform.price.deadline_date}<br/>`;
|
||||
}
|
||||
description += '<br/>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (item.price) {
|
||||
description += `平台: ${platform.toUpperCase()}<br/>`;
|
||||
if (item.price.discount) {
|
||||
description += `折扣力度: ${(100 - item.price.discount) / 10}折<br/>`;
|
||||
}
|
||||
if (item.price.initial && item.price.discount) {
|
||||
const current = Math.round((item.price.initial * (100 - item.price.discount)) / 100);
|
||||
description += `当前价格: ${current}${item.price.discount === item.price.lowest_discount ? '[史低]' : ''} `;
|
||||
}
|
||||
if (item.price.initial) {
|
||||
description += `原价: ${item.price.initial}<br/>`;
|
||||
}
|
||||
if (item.score) {
|
||||
description += `评分: ${item.score}<br/>`;
|
||||
}
|
||||
description += '<br/>';
|
||||
}
|
||||
}
|
||||
let link = `https://xiaoheihe.cn/games/detail/${item.steam_appid}`;
|
||||
if (platform === 'pc') {
|
||||
link = `https://store.steampowered.com/app/${item.steam_appid}`;
|
||||
}
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
link,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
const got = require('@/utils/got');
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const response = await got({
|
||||
method: 'get',
|
||||
url: `https://api.xiaoheihe.cn/maxnews/app/list?tag=-1&offset=0&limit=30&rec_mark=timeline&heybox_id=12777814&imei=867252032615972&os_type=Android&os_version=9&version=1.1.55&_time=1551801017&hkey=b28cd7a1cba463b4d9176ba2f8f42d35`,
|
||||
});
|
||||
const data = response.data.result.filter((item) => item.content_type !== 7);
|
||||
|
||||
const result = await Promise.all(
|
||||
data.map(async (item) => {
|
||||
const newsId = item.newsid;
|
||||
const cacheKey = `xiaoheihe_${newsId}`;
|
||||
|
||||
// 最终需要返回的对象
|
||||
const news = {
|
||||
title: item.title,
|
||||
pubDate: item.data,
|
||||
link: `https://api.xiaoheihe.cn/maxnews/app/share/detail/${newsId}`,
|
||||
guid: cacheKey,
|
||||
};
|
||||
|
||||
// 判断缓存中是否存在
|
||||
const cacheValue = await ctx.cache.get(cacheKey);
|
||||
if (cacheValue) {
|
||||
news.description = cacheValue;
|
||||
} else {
|
||||
const newsUrl = `https://api.xiaoheihe.cn/maxnews/app/detail/${newsId}?from_tag=-1&newsid=${newsId}&rec_mark=timeline&pos=2&index=1&page_tab=1&from_recommend_list=3&h_src=LTE%3D&os_type=Android&os_version=9&hkey=c25f13a25787311aca69d031280f4682&imei=867252032615972&version=1.2.57&_time=1552533062&heybox_id=12777814`;
|
||||
const article = await got({
|
||||
method: 'get',
|
||||
url: newsUrl,
|
||||
});
|
||||
// 解析html内容
|
||||
const $ = cheerio.load(article.data);
|
||||
const content = $('.article-content').html().replaceAll('data-original', 'src');
|
||||
// 存放到缓存区
|
||||
ctx.cache.set(cacheKey, content);
|
||||
news.description = content;
|
||||
}
|
||||
|
||||
return news;
|
||||
})
|
||||
);
|
||||
|
||||
ctx.state.data = {
|
||||
title: `小黑盒游戏新闻`,
|
||||
link: `https://xiaoheihe.cn/community/index`,
|
||||
item: result,
|
||||
};
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
const got = require('@/utils/got');
|
||||
const cheerio = require('cheerio');
|
||||
const { parseDate } = require('@/utils/parse-date');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const { id } = ctx.params;
|
||||
const limit = Number.parseInt(ctx.query.limit) || 10;
|
||||
const rootUrl = 'https://api.xiaoheihe.cn';
|
||||
const currentUrl = `${rootUrl}/bbs/web/profile/post/links?userid=${id}&limit=${limit}&offset=0&version=999.0.0`;
|
||||
|
||||
const data = await got(currentUrl).then((response) => response.data);
|
||||
const username = data.user.username;
|
||||
const articleList = data.post_links.map((post) => ({
|
||||
title: post.title || post.description,
|
||||
pubDate: parseDate(post.create_at * 1000),
|
||||
link: post.share_url,
|
||||
linkId: post.linkid,
|
||||
}));
|
||||
|
||||
const items = await Promise.all(
|
||||
articleList.map((item) =>
|
||||
ctx.cache.tryGet(item.link, async () => {
|
||||
const url = `${rootUrl}/bbs/app/api/share/data/?offset=0&limit=3&link_id=${item.linkId}&os_type=web`;
|
||||
const link = await got(url).then((response) => response.data.link);
|
||||
const content = link.content;
|
||||
const video = link.video;
|
||||
let description;
|
||||
|
||||
for (const item of content) {
|
||||
if (item.type === 'html') {
|
||||
const $ = cheerio.load(item.text, { decodeEntities: false });
|
||||
$('[data-gameid]').each((i, e) => $(e).remove());
|
||||
$('[data-original]').each((i, e) => $(e).replaceWith(`<img src = "${$(e).attr('data-original')}">`));
|
||||
description = $.html();
|
||||
}
|
||||
}
|
||||
if (video) {
|
||||
description = `<video src="${video.video_url}" poster="${video.thumb}" controls="controls"></video>`;
|
||||
}
|
||||
item.description = description;
|
||||
return item;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
ctx.state.data = {
|
||||
title: `${username} 的动态`,
|
||||
link: `https://xiaoheihe.cn/community/user/${id}/post_list`,
|
||||
item: items,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Route } from '@/types';
|
||||
import got from '@/utils/got';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/discount/:platform',
|
||||
categories: ['game'],
|
||||
example: '/xiaoheihe/discount/pc',
|
||||
parameters: { platform: '平台分类,见下表' },
|
||||
features: {
|
||||
requireConfig: false,
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
name: '游戏折扣',
|
||||
maintainers: ['tssujt'],
|
||||
handler,
|
||||
description: `| PC | Switch | PSN | Xbox |
|
||||
| ----- | ------ | ----- | ----- |
|
||||
| pc | switch | psn | xbox |`,
|
||||
};
|
||||
|
||||
const PLATFORM_MAP = {
|
||||
pc: 'PC',
|
||||
switch: 'Switch',
|
||||
psn: 'PSN',
|
||||
xbox: 'Xbox',
|
||||
};
|
||||
|
||||
async function handler(ctx) {
|
||||
const platform = ctx.req.param('platform');
|
||||
|
||||
const response = await got({
|
||||
method: 'get',
|
||||
url: `https://api.xiaoheihe.cn/game/get_game_list_v3?sort_type=discount&filter_head=${platform}&offset=0&limit=30&os_type=web`,
|
||||
});
|
||||
const data = response.data.result.games;
|
||||
|
||||
const items = data.map((item) => {
|
||||
const title = `${item.name}${item.name_en ? '/' + item.name_en : ''}`;
|
||||
let description = `
|
||||
<img src="${item.image}"/> <br/>
|
||||
`;
|
||||
if (item.platform_infos) {
|
||||
for (const platform of item.platform_infos) {
|
||||
if (platform.price) {
|
||||
if (platform.key) {
|
||||
description += `平台: ${platform.key}<br/>>`;
|
||||
}
|
||||
if (platform.price.current) {
|
||||
description += `当前价格: ${platform.price.current}${platform.price.discount === platform.price.lowest_discount ? '[史低]' : ''}<br/>`;
|
||||
}
|
||||
if (platform.price.initial) {
|
||||
description += `原价: ${platform.price.initial}<br/>`;
|
||||
}
|
||||
if (platform.price.discount_desc) {
|
||||
description += `折扣力度: ${platform.price.discount_desc}<br/>`;
|
||||
}
|
||||
if (platform.price.deadline_date) {
|
||||
description += `截止时间: ${platform.price.deadline_date}<br/>`;
|
||||
}
|
||||
description += '<br/>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (item.price) {
|
||||
description += `平台: ${platform.toUpperCase()}<br/>`;
|
||||
if (item.price.discount) {
|
||||
description += `折扣力度: ${(100 - item.price.discount) / 10}折<br/>`;
|
||||
}
|
||||
if (item.price.initial && item.price.discount) {
|
||||
const current = Math.round((item.price.initial * (100 - item.price.discount)) / 100);
|
||||
description += `当前价格: ${current}${item.price.discount === item.price.lowest_discount ? '[史低]' : ''} `;
|
||||
}
|
||||
if (item.price.initial) {
|
||||
description += `原价: ${item.price.initial}<br/>`;
|
||||
}
|
||||
if (item.score) {
|
||||
description += `评分: ${item.score}<br/>`;
|
||||
}
|
||||
description += '<br/>';
|
||||
}
|
||||
}
|
||||
let link = `https://api.xiaoheihe.cn/game/share_game_detail?appid=${item.steam_appid}`;
|
||||
if (platform === 'pc') {
|
||||
link = `https://store.steampowered.com/app/${item.steam_appid}`;
|
||||
}
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
link,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
title: `小黑盒 ${PLATFORM_MAP[platform]} 游戏折扣`,
|
||||
link: `https://xiaoheihe.cn`,
|
||||
item: items,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: '小黑盒',
|
||||
url: 'xiaoheihe.cn',
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Route } from '@/types';
|
||||
import cache from '@/utils/cache';
|
||||
import got from '@/utils/got';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import { calculate } from './util';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/news',
|
||||
categories: ['game'],
|
||||
example: '/xiaoheihe/news',
|
||||
features: {
|
||||
requireConfig: false,
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
name: '游戏新闻',
|
||||
maintainers: ['tssujt'],
|
||||
handler,
|
||||
};
|
||||
|
||||
async function handler() {
|
||||
const response = await got({
|
||||
method: 'get',
|
||||
url: `https://api.xiaoheihe.cn/bbs/app/feeds/news?lang=zh-cn&limit=20&offset=0&tag=-1&version=1.3.303`,
|
||||
});
|
||||
const data = response.data.result.links.filter((item) => item.linkid !== undefined);
|
||||
let items = data.map((item) => ({
|
||||
linkId: item.linkid,
|
||||
link: `https://api.xiaoheihe.cn/v3/bbs/app/api/web/share?link_id=${item.linkid}`,
|
||||
title: item.title,
|
||||
pubDate: parseDate(item.modify_at * 1000),
|
||||
description: item.description,
|
||||
}));
|
||||
|
||||
items = await Promise.all(
|
||||
items.map((item) =>
|
||||
cache.tryGet(item.link, async () => {
|
||||
const dataUrl = calculate(
|
||||
`https://api.xiaoheihe.cn/bbs/app/api/share/data/?os_type=web&app=heybox&client_type=mobile&version=999.0.3&x_client_type=web&x_os_type=Mac&x_app=heybox&heybox_id=-1&offset=0&limit=3&link_id=${item.linkId}&use_concept_type=`
|
||||
);
|
||||
const dataResponse = await got({
|
||||
method: 'get',
|
||||
url: dataUrl,
|
||||
});
|
||||
item.description = dataResponse.data.link.content[0].text;
|
||||
delete item.linkId;
|
||||
return item;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
title: `小黑盒游戏新闻`,
|
||||
link: `https://xiaoheihe.cn`,
|
||||
item: items,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Route } from '@/types';
|
||||
import cache from '@/utils/cache';
|
||||
import got from '@/utils/got';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import { calculate } from './util';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/user/:id',
|
||||
categories: ['game'],
|
||||
example: '/xiaoheihe/user/30664023',
|
||||
parameters: { id: '用户 ID' },
|
||||
features: {
|
||||
requireConfig: false,
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
name: '用户动态',
|
||||
maintainers: ['tssujt'],
|
||||
handler,
|
||||
};
|
||||
|
||||
async function handler(ctx) {
|
||||
const userId = ctx.req.param('id');
|
||||
|
||||
const userResponse = await got({
|
||||
method: 'get',
|
||||
url: `https://api.xiaoheihe.cn/bbs/app/profile/user/profile?lang=zh-cn&version=1.3.303&userid=${userId}`,
|
||||
});
|
||||
const username = userResponse.data.result.account_detail.username;
|
||||
|
||||
const response = await got({
|
||||
method: 'get',
|
||||
url: `https://api.xiaoheihe.cn/bbs/app/profile/events?lang=zh-cn&version=1.3.303&userid=${userId}&list_type=moment`,
|
||||
});
|
||||
const data = response.data.result.moments.filter((item) => item.linkid !== undefined);
|
||||
|
||||
let items = data.map((item) => ({
|
||||
linkId: item.linkid,
|
||||
link: `https://api.xiaoheihe.cn/v3/bbs/app/api/web/share?link_id=${item.linkid}`,
|
||||
title: item.title,
|
||||
pubDate: parseDate(item.modify_at * 1000),
|
||||
description: item.description,
|
||||
}));
|
||||
items = await Promise.all(
|
||||
items.map((item) =>
|
||||
cache.tryGet(item.link, async () => {
|
||||
const dataUrl = calculate(
|
||||
`https://api.xiaoheihe.cn/bbs/app/api/share/data/?os_type=web&app=heybox&client_type=mobile&version=999.0.3&x_client_type=web&x_os_type=Mac&x_app=heybox&heybox_id=-1&offset=0&limit=3&link_id=${item.linkId}&use_concept_type=`
|
||||
);
|
||||
const dataResponse = await got({
|
||||
method: 'get',
|
||||
url: dataUrl,
|
||||
});
|
||||
item.description = dataResponse.data.link.content[0].text;
|
||||
delete item.linkId;
|
||||
return item;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
title: `${username} 的动态`,
|
||||
link: `https://xiaoheihe.cn`,
|
||||
item: items,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copied from https://github.com/huandu/heybox-url
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const dict = 'JKMNPQRTX1234OABCDFG56789H';
|
||||
|
||||
const md5 = (str) => {
|
||||
const h = createHash('md5');
|
||||
h.update(str);
|
||||
return h.digest();
|
||||
};
|
||||
|
||||
function c0(v) {
|
||||
return c1(v) ^ c2(v) ^ c3(v);
|
||||
}
|
||||
|
||||
function c1(v) {
|
||||
return c2(c3(convertByte(v)));
|
||||
}
|
||||
|
||||
function c2(v) {
|
||||
return c3(convertByte(v));
|
||||
}
|
||||
|
||||
function c3(v) {
|
||||
return convertByte(v) ^ v;
|
||||
}
|
||||
|
||||
function convertByte(v) {
|
||||
return v & 0x80 ? 0xff & ((v << 1) ^ 0x1b) : v << 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate checksum for a 4-byte data.
|
||||
* @param {number[]} data
|
||||
* @returns {number}
|
||||
*/
|
||||
const checksum = (data) =>
|
||||
[c0(data[0]) ^ c1(data[1]) ^ c2(data[2]) ^ c3(data[3]), c3(data[0]) ^ c0(data[1]) ^ c1(data[2]) ^ c2(data[3]), c2(data[0]) ^ c3(data[1]) ^ c0(data[2]) ^ c1(data[3]), c1(data[0]) ^ c2(data[1]) ^ c3(data[2]) ^ c0(data[3])].reduce(
|
||||
(prev, value) => prev + value
|
||||
) % 100;
|
||||
|
||||
export const calculate = (url, timestamp = 0, nonce = '') => {
|
||||
timestamp ||= Math.trunc(Date.now() / 1000);
|
||||
nonce ||= md5(Math.random().toString()).toString('hex').toUpperCase();
|
||||
|
||||
const { pathname } = new URL(url);
|
||||
const ts = timestamp + 1;
|
||||
const u = '/' + pathname.split('/').filter(Boolean).join('/') + '/';
|
||||
|
||||
let key = '';
|
||||
const nonceHash = md5((nonce + dict).replaceAll(/\D/g, ''))
|
||||
.toString('hex')
|
||||
.toLowerCase();
|
||||
const rnd = md5(ts + u + nonceHash)
|
||||
.toString('hex')
|
||||
.replaceAll(/\D/g, '')
|
||||
.slice(0, 9)
|
||||
.padEnd(9, '0');
|
||||
|
||||
for (let c = +rnd, i = 0; i < 5; i++) {
|
||||
const u = c % dict.length;
|
||||
c = Math.trunc(c / dict.length);
|
||||
key += dict[u];
|
||||
}
|
||||
|
||||
const suffix = checksum([...key].slice(-4).map((c) => c.codePointAt(0)))
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
|
||||
const query = `hkey=${key}${suffix}&_time=${timestamp}&nonce=${nonce}`;
|
||||
const urlObj = new URL(url);
|
||||
urlObj.search += urlObj.search ? '&' + query : query;
|
||||
return urlObj.toString();
|
||||
};
|
||||
Reference in New Issue
Block a user