feat(route): add taptap.io / TapTap 国际版 (#10596)
* feat(route): taptap, add docs for (/intl)/(changelog|review) * feat(route): taptap, add /intl prefix on /(changelog|review)
This commit is contained in:
parent
b9e370f4b6
commit
1902fba510
|
|
@ -281,6 +281,52 @@ Steam provides some official RSS feeds:
|
|||
|
||||
<RouteEn author="whtsky" example="/steamgifts/discussions" path="/steamgifts/discussions/:category?" :paramsDesc="['category name, default to All']"/>
|
||||
|
||||
## TapTap International
|
||||
|
||||
::: warning Warning
|
||||
|
||||
Due to the regional restrictions, an RSSHub deployment in China Mainland may not work on accessing the TapTap International Website.
|
||||
|
||||
:::
|
||||
|
||||
### Game Topics
|
||||
|
||||
::: tip Tips
|
||||
|
||||
Unlike TapTap China Mainland Website, the International Website has no BBS.
|
||||
|
||||
:::
|
||||
|
||||
### Game's Changelog
|
||||
|
||||
<RouteEn author="hoilc ETiV" example="/taptap/intl/changelog/191001/zh_TW" path="/taptap/intl/changelog/:id/:lang?" :paramsDesc="['Game\'s App ID, you may find it from the URL of the Game', 'Language, checkout the table below for possible values, default is `en_US`']">
|
||||
|
||||
#### Language Code
|
||||
|
||||
| English (US) | 繁體中文 | 한국어 | 日本語 |
|
||||
| ----- | ----- | ----- | ----- |
|
||||
| en_US | zh_TW | ko_KR | ja_JP |
|
||||
|
||||
</RouteEn>
|
||||
|
||||
### Ratings & Reviews
|
||||
|
||||
<RouteEn author="hoilc TonyRL ETiV" example="/taptap/intl/review/82354/new/zh_TW" path="/taptap/intl/review/:id/:order?/:lang?" :paramsDesc="['Game\'s App ID, you may find it from the URL of the Game', 'Sort Method, you may use `new` as the **Most Recent**, use `default` or leave it empty for the **Most Relevant**', 'Language, checkout the table below for possible values, default is `en_US`']">
|
||||
|
||||
#### Sort Method
|
||||
|
||||
| Most Relevant | Most Recent |
|
||||
| -------------- | ---- |
|
||||
| default | new |
|
||||
|
||||
#### Language Code
|
||||
|
||||
| English (US) | 繁體中文 | 한국어 | 日本語 |
|
||||
| ----- | ----- | ----- | ----- |
|
||||
| en_US | zh_TW | ko_KR | ja_JP |
|
||||
|
||||
</RouteEn>
|
||||
|
||||
## War Thunder
|
||||
|
||||
### News
|
||||
|
|
@ -290,4 +336,4 @@ Steam provides some official RSS feeds:
|
|||
News data from https://warthunder.com/en/news/
|
||||
The year, month and day provided under UTC time zone are the same as the official website, so please ignore the specific time!!!
|
||||
|
||||
</RouteEn>
|
||||
</RouteEn>
|
||||
|
|
|
|||
35
docs/game.md
35
docs/game.md
|
|
@ -461,7 +461,8 @@ Steam 新闻中心支持官方的 RSS 订阅:
|
|||
|
||||
::: warning 注意
|
||||
|
||||
由于区域限制,需要在有国内 IP 的机器上自建才能正常获取 RSS
|
||||
由于区域限制,需要在有国内 IP 的机器上自建才能正常获取 RSS。\
|
||||
而对于《TapTap 国际版》则需要部署在具有海外出口的 IP 上才可正常获取 RSS。
|
||||
|
||||
:::
|
||||
|
||||
|
|
@ -481,7 +482,19 @@ Steam 新闻中心支持官方的 RSS 订阅:
|
|||
|
||||
### 游戏更新
|
||||
|
||||
<Route author="hoilc" example="/taptap/changelog/142793" path="/taptap/changelog/:id" :paramsDesc="['游戏 ID,游戏主页 URL 中获取']"/>
|
||||
<Route author="hoilc ETiV" example="/taptap/changelog/60809/en_US" path="/taptap/changelog/:id/:lang?" :paramsDesc="['游戏 ID,游戏主页 URL 中获取', '语言,默认使用 `zh_CN`,亦可使用 `en_US`']"/>
|
||||
|
||||
### 游戏更新 - 国际版
|
||||
|
||||
<Route author="hoilc ETiV" example="/taptap/intl/changelog/191001/zh_TW" path="/taptap/intl/changelog/:id/:lang?" :paramsDesc="['游戏 ID,游戏主页 URL 中获取', '语言代码,默认使用 `en_US`,可选项目见下表']">
|
||||
|
||||
#### 语言代码
|
||||
|
||||
| English (US) | 繁體中文 | 한국어 | 日本語 |
|
||||
| ------------ | ----- | ----- | ----- |
|
||||
| en_US | zh_TW | ko_KR | ja_JP |
|
||||
|
||||
</Route>
|
||||
|
||||
### 游戏评价
|
||||
|
||||
|
|
@ -493,6 +506,24 @@ Steam 新闻中心支持官方的 RSS 订阅:
|
|||
|
||||
</Route>
|
||||
|
||||
### 游戏评价 - 国际版
|
||||
|
||||
<Route author="hoilc TonyRL ETiV" example="/taptap/intl/review/82354/new" path="/taptap/intl/review/:id/:order?/:lang?" :paramsDesc="['游戏 ID,游戏主页 URL 中获取', '排序方式,空为默认排序,可选如下', '语言代码,默认使用 `en_US`,可选项目见下表']">
|
||||
|
||||
#### 排序方式
|
||||
|
||||
| 最相关 | 最新 |
|
||||
| ------- | --- |
|
||||
| default | new |
|
||||
|
||||
#### 语言代码
|
||||
|
||||
| English (US) | 繁體中文 | 한국어 | 日本語 |
|
||||
| ------------ | ----- | ----- | ----- |
|
||||
| en_US | zh_TW | ko_KR | ja_JP |
|
||||
|
||||
</Route>
|
||||
|
||||
## War Thunder
|
||||
|
||||
### 英文 News
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
const got = require('@/utils/got');
|
||||
const cheerio = require('cheerio');
|
||||
const { parseDate } = require('@/utils/parse-date');
|
||||
const { getRootUrl, appDetail, X_UA } = require('./utils');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const is_intl = ctx.request.url.indexOf('/intl/') === 0;
|
||||
const id = ctx.params.id;
|
||||
const lang = ctx.params.lang ?? (is_intl ? 'en_US' : 'zh_CN');
|
||||
|
||||
const url = `https://www.taptap.com/app/${id}`;
|
||||
const url = `${getRootUrl(is_intl)}/app/${id}`;
|
||||
|
||||
const app_response = await got.get(url);
|
||||
const $ = cheerio.load(app_response.data);
|
||||
const app_detail = await appDetail(id, lang, is_intl);
|
||||
|
||||
const app_img = $('.header-icon-body > img').attr('src');
|
||||
const app_name = $('.breadcrumb > li.active').text();
|
||||
const app_description = $('.body-description-paragraph').text();
|
||||
const app_img = app_detail.app.icon.original_url;
|
||||
const app_name = app_detail.app.title;
|
||||
const app_description = `${app_name} by ${app_detail.app.developers.map((item) => item.name).join(' & ')}`;
|
||||
|
||||
const response = await got({
|
||||
method: 'get',
|
||||
url: `https://www.taptap.com/ajax/apk/v1/list-by-app?app_id=${id}&from=0&limit=10`,
|
||||
url: `${getRootUrl(is_intl)}/webapiv2/apk/v1/list-by-app?app_id=${id}&from=0&limit=10&${X_UA(lang)}`,
|
||||
headers: {
|
||||
Referer: url,
|
||||
},
|
||||
|
|
@ -30,9 +31,9 @@ module.exports = async (ctx) => {
|
|||
link: url,
|
||||
image: app_img,
|
||||
item: list.map((item) => ({
|
||||
title: item.version_label,
|
||||
title: `${app_name} / ${item.version_label}`,
|
||||
description: item.whatsnew.text,
|
||||
pubDate: parseDate(item.update_date),
|
||||
pubDate: parseDate(item.update_date * 1000),
|
||||
link: url,
|
||||
guid: item.version_label,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
module.exports = {
|
||||
'/changelog/:id': ['hoilc'],
|
||||
'/changelog/:id/:lang?': ['hoilc', 'ETiV'],
|
||||
'/review/:id/:order?/:lang?': ['hoilc', 'TonyRL'],
|
||||
'/topic/:id/:type?/:sort?/:lang?': ['hoilc', 'TonyRL'],
|
||||
'/intl/changelog/:id/:lang?': ['hoilc', 'ETiV'],
|
||||
'/intl/review/:id/:order?/:lang?': ['hoilc', 'TonyRL', 'ETiV'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,4 +22,21 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
},
|
||||
'taptap.io': {
|
||||
_name: 'TapTap International',
|
||||
'.': [
|
||||
{
|
||||
title: 'Changelog',
|
||||
docs: 'https://docs.rsshub.app/game.html#taptap',
|
||||
source: ['/app/:id'],
|
||||
target: '/taptap/intl/changelog/:id',
|
||||
},
|
||||
{
|
||||
title: 'Ratings & Reviews',
|
||||
docs: 'https://docs.rsshub.app/game.html#taptap',
|
||||
source: ['/app/:id/review', '/app/:id'],
|
||||
target: '/taptap/intl/review/:id',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,69 +1,131 @@
|
|||
const got = require('@/utils/got');
|
||||
const { parseDate } = require('@/utils/parse-date');
|
||||
const { rootUrl, X_UA } = require('./utils');
|
||||
const { getRootUrl, appDetail, X_UA } = require('./utils');
|
||||
|
||||
const sortMap = {
|
||||
default: {
|
||||
en_US: 'Default',
|
||||
zh_CN: '预设',
|
||||
zh_TW: '預設',
|
||||
},
|
||||
new: {
|
||||
en_US: 'Latest',
|
||||
zh_CN: '最新',
|
||||
zh_TW: '最新',
|
||||
},
|
||||
hot: {
|
||||
en_US: 'Popular',
|
||||
zh_CN: '热门',
|
||||
zh_TW: '熱門',
|
||||
},
|
||||
spent: {
|
||||
en_US: 'Play Time',
|
||||
zh_CN: '游戏时长',
|
||||
zh_TW: '遊戲時長',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const id = ctx.params.id;
|
||||
const order = ctx.params.order ?? 'default';
|
||||
const lang = ctx.params.lang ?? 'zh_CN';
|
||||
const intlSortMap = {
|
||||
default: {
|
||||
en_US: 'Most Relevant',
|
||||
zh_TW: '最相關',
|
||||
ja_JP: '関連性が高い',
|
||||
ko_KR: '관련도순',
|
||||
},
|
||||
new: {
|
||||
en_US: 'Most Recent',
|
||||
zh_TW: '最新',
|
||||
ja_JP: '最も最近',
|
||||
ko_KR: '최근순',
|
||||
},
|
||||
};
|
||||
|
||||
let url = `${rootUrl}/webapiv2/review/v2/by-app?app_id=${id}&limit=10`;
|
||||
let sortParam;
|
||||
if (order === 'new' || order === 'hot' || order === 'spent') {
|
||||
sortParam = `sort=${order}`;
|
||||
url += `&${sortParam}`;
|
||||
const makeSortParam = (isIntl, order) => {
|
||||
if (isIntl) {
|
||||
if (order === 'new') {
|
||||
return `sort=${order}`;
|
||||
}
|
||||
} else {
|
||||
if (order === 'new' || order === 'hot' || order === 'spent') {
|
||||
return `sort=${order}`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const fetchMainlandItems = async (params) => {
|
||||
const id = params.id;
|
||||
const order = params.order ?? 'default';
|
||||
const lang = params.lang ?? 'zh_CN';
|
||||
|
||||
let url = `${getRootUrl(false)}/webapiv2/review/v2/by-app?app_id=${id}&limit=10`;
|
||||
url += `&${makeSortParam(false, order)}`;
|
||||
url += `&${X_UA(lang)}`;
|
||||
|
||||
const reviews_list_response = await got(url);
|
||||
const reviews_list = reviews_list_response.data;
|
||||
const reviews_list = reviews_list_response.data.data.list;
|
||||
|
||||
const app_img = reviews_list.data.list[0].moment.app.icon.url;
|
||||
const app_name = reviews_list.data.list[0].moment.app.title;
|
||||
|
||||
const items = reviews_list.data.list.map((review) => {
|
||||
return reviews_list.map((review) => {
|
||||
const author = review.moment.author.user.name;
|
||||
const score = review.moment.extended_entities.reviews[0].score;
|
||||
return {
|
||||
title: `${author} - ${score}星`,
|
||||
author,
|
||||
description: review.moment.extended_entities.reviews[0].contents.text,
|
||||
link: `${rootUrl}/review/${review.moment.extended_entities.reviews[0].id}`,
|
||||
link: `${getRootUrl(false)}/review/${review.moment.extended_entities.reviews[0].id}`,
|
||||
pubDate: parseDate(review.moment.extended_entities.reviews[0].created_time * 1000),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const fetchIntlItems = async (params) => {
|
||||
const id = params.id;
|
||||
const order = params.order ?? 'default';
|
||||
const lang = params.lang ?? 'en_US';
|
||||
|
||||
let url = `${getRootUrl(true)}/webapiv2/feeds/v1/app-ratings?app_id=${id}&limit=10`;
|
||||
url += `&${makeSortParam(true, order)}`;
|
||||
url += `&${X_UA(lang)}`;
|
||||
|
||||
const reviews_list_response = await got(url);
|
||||
const reviews_list = reviews_list_response.data.data.list;
|
||||
|
||||
return reviews_list.map((review) => {
|
||||
const author = review.post.user.name;
|
||||
const score = review.post.list_fields.app_ratings[id].score;
|
||||
return {
|
||||
title: `${author} - ${score}星`,
|
||||
author,
|
||||
description: review.post.list_fields.summary || review.post.list_fields.title,
|
||||
link: `${getRootUrl(true)}/post/${review.post.id_str}`,
|
||||
pubDate: parseDate(review.post.published_time * 1000),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const is_intl = ctx.request.url.indexOf('/intl/') === 0;
|
||||
const id = ctx.params.id;
|
||||
const order = ctx.params.order ?? 'default';
|
||||
const lang = ctx.params.lang ?? (is_intl ? 'en_US' : 'zh_CN');
|
||||
|
||||
const app_detail = await appDetail(id, lang, is_intl);
|
||||
const app_img = app_detail.app.icon.original_url;
|
||||
const app_name = app_detail.app.title;
|
||||
|
||||
const items = is_intl ? await fetchIntlItems(ctx.params) : await fetchMainlandItems(ctx.params);
|
||||
|
||||
ctx.state.data = {
|
||||
title: `TapTap 评价 ${app_name} - ${sortMap[order][lang]}排序`,
|
||||
link: `${rootUrl}/app/${id}/review${sortParam ? `?${sortParam}` : ''}`,
|
||||
title: `TapTap 评价 ${app_name} - ${(is_intl ? intlSortMap : sortMap)[order][lang]}排序`,
|
||||
link: `${getRootUrl(is_intl)}/app/${id}/review?${makeSortParam(is_intl, order)}`,
|
||||
image: app_img,
|
||||
item: items,
|
||||
};
|
||||
|
||||
ctx.state.json = {
|
||||
title: `TapTap 评价 ${app_name} - ${sortMap[order][lang]}排序`,
|
||||
link: `${rootUrl}/app/${id}/review${sortParam ? `?${sortParam}` : ''}`,
|
||||
title: `TapTap 评价 ${app_name} - ${(is_intl ? intlSortMap : sortMap)[order][lang]}排序`,
|
||||
link: `${getRootUrl(is_intl)}/app/${id}/review?${makeSortParam(is_intl, order)}`,
|
||||
image: app_img,
|
||||
item: items,
|
||||
reviews_list,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
module.exports = (router) => {
|
||||
router.get('/changelog/:id', require('./changelog'));
|
||||
router.get('/changelog/:id/:lang?', require('./changelog'));
|
||||
router.get('/review/:id/:order?/:lang?', require('./review'));
|
||||
router.get('/topic/:id/:type?/:sort?/:lang?', require('./topic'));
|
||||
// TapTap.io, the international website
|
||||
router.get('/intl/changelog/:id/:lang?', require('./changelog'));
|
||||
router.get('/intl/review/:id/:order?/:lang?', require('./review'));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const got = require('@/utils/got');
|
||||
const { parseDate } = require('@/utils/parse-date');
|
||||
const { rootUrl, X_UA, appDetail, imagePost, topicPost, videoPost } = require('./utils');
|
||||
const { getRootUrl, X_UA, appDetail, imagePost, topicPost, videoPost } = require('./utils');
|
||||
|
||||
const typeMap = {
|
||||
feed: {
|
||||
|
|
@ -30,7 +30,7 @@ module.exports = async (ctx) => {
|
|||
const groupId = appData.group.id;
|
||||
const app_img = appData.app.icon.original_url || appData.app.icon.url;
|
||||
const app_name = appData.app.title;
|
||||
const url = `${rootUrl}/webapiv2/feed/v6/by-group?group_id=${groupId}&type=${type}&sort=${sort}&${X_UA(lang)}`;
|
||||
const url = `${getRootUrl(false)}/webapiv2/feed/v6/by-group?group_id=${groupId}&type=${type}&sort=${sort}&${X_UA(lang)}`;
|
||||
|
||||
const topics_list_response = await got(url);
|
||||
const topics_list = topics_list_response.data;
|
||||
|
|
@ -68,14 +68,14 @@ module.exports = async (ctx) => {
|
|||
|
||||
ctx.state.data = {
|
||||
title: `${app_name} - ${typeMap[type][lang]} - TapTap 论坛`,
|
||||
link: `${rootUrl}/app/${appId}/topic?type=${type}&sort=${sort}`,
|
||||
link: `${getRootUrl(false)}/app/${appId}/topic?type=${type}&sort=${sort}`,
|
||||
image: app_img,
|
||||
item: out.filter((item) => item !== ''),
|
||||
};
|
||||
|
||||
ctx.state.json = {
|
||||
title: `${app_name} - ${typeMap[type][lang]} - TapTap 论坛`,
|
||||
link: `${rootUrl}/app/${appId}/topic?type=${type}&sort=${sort}`,
|
||||
link: `${getRootUrl(false)}/app/${appId}/topic?type=${type}&sort=${sort}`,
|
||||
image: app_img,
|
||||
item: out.filter((item) => item !== ''),
|
||||
appId,
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@ const cheerio = require('cheerio');
|
|||
const path = require('path');
|
||||
const { art } = require('@/utils/render');
|
||||
|
||||
const rootUrl = 'https://www.taptap.com';
|
||||
// Please do not change %26 to &
|
||||
const X_UA = (lang = 'zh_CN') => `X-UA=V=1%26PN=WebApp%26VN=0.1.0%26LANG=${lang}%26PLT=PC`;
|
||||
|
||||
const appDetail = async (appId, lang = 'zh_CN') => {
|
||||
const { data } = await got(`${rootUrl}/webapiv2/group/v1/detail?app_id=${appId}&${X_UA(lang)}`, {
|
||||
const getRootUrl = (isIntl = false) => (isIntl ? 'https://www.taptap.io' : 'https://www.taptap.com');
|
||||
|
||||
const appDetail = async (appId, lang = 'zh_CN', isIntl = false) => {
|
||||
const { data } = await got(`${getRootUrl(isIntl)}/webapiv2/group/v1/detail?app_id=${appId}&${X_UA(lang)}`, {
|
||||
headers: {
|
||||
Referer: `${rootUrl}/app/${appId}`,
|
||||
Referer: `${getRootUrl(isIntl)}/app/${appId}`,
|
||||
},
|
||||
});
|
||||
return data.data;
|
||||
|
|
@ -22,9 +23,9 @@ const imagePost = (images) =>
|
|||
});
|
||||
|
||||
const topicPost = async (appId, topicId, lang = 'zh_CN') => {
|
||||
const res = await got(`${rootUrl}/webapiv2/topic/v1/detail?id=${topicId}&${X_UA(lang)}`, {
|
||||
const res = await got(`${getRootUrl(false)}/webapiv2/topic/v1/detail?id=${topicId}&${X_UA(lang)}`, {
|
||||
headers: {
|
||||
Referer: `${rootUrl}/app/${appId}`,
|
||||
Referer: `${getRootUrl(false)}/app/${appId}`,
|
||||
},
|
||||
});
|
||||
const $ = cheerio.load(res.data.data.first_post.contents.text, null, false);
|
||||
|
|
@ -44,7 +45,7 @@ const videoPost = (video) =>
|
|||
});
|
||||
|
||||
module.exports = {
|
||||
rootUrl,
|
||||
getRootUrl,
|
||||
X_UA,
|
||||
appDetail,
|
||||
imagePost,
|
||||
|
|
|
|||
Loading…
Reference in New Issue