diff --git a/docs/en/game.md b/docs/en/game.md index 3acc0d156..66a24c264 100755 --- a/docs/en/game.md +++ b/docs/en/game.md @@ -281,6 +281,52 @@ Steam provides some official RSS feeds: +## 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 + + + +#### Language Code + +| English (US) | 繁體中文 | 한국어 | 日本語 | +| ----- | ----- | ----- | ----- | +| en_US | zh_TW | ko_KR | ja_JP | + + + +### Ratings & Reviews + + + +#### Sort Method + +| Most Relevant | Most Recent | +| -------------- | ---- | +| default | new | + +#### Language Code + +| English (US) | 繁體中文 | 한국어 | 日本語 | +| ----- | ----- | ----- | ----- | +| en_US | zh_TW | ko_KR | ja_JP | + + + ## 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!!! - \ No newline at end of file + diff --git a/docs/game.md b/docs/game.md index c345f0de4..f6cf3d97a 100644 --- a/docs/game.md +++ b/docs/game.md @@ -461,7 +461,8 @@ Steam 新闻中心支持官方的 RSS 订阅: ::: warning 注意 -由于区域限制,需要在有国内 IP 的机器上自建才能正常获取 RSS +由于区域限制,需要在有国内 IP 的机器上自建才能正常获取 RSS。\ +而对于《TapTap 国际版》则需要部署在具有海外出口的 IP 上才可正常获取 RSS。 ::: @@ -481,7 +482,19 @@ Steam 新闻中心支持官方的 RSS 订阅: ### 游戏更新 - + + +### 游戏更新 - 国际版 + + + +#### 语言代码 + +| English (US) | 繁體中文 | 한국어 | 日本語 | +| ------------ | ----- | ----- | ----- | +| en_US | zh_TW | ko_KR | ja_JP | + + ### 游戏评价 @@ -493,6 +506,24 @@ Steam 新闻中心支持官方的 RSS 订阅: +### 游戏评价 - 国际版 + + + +#### 排序方式 + +| 最相关 | 最新 | +| ------- | --- | +| default | new | + +#### 语言代码 + +| English (US) | 繁體中文 | 한국어 | 日本語 | +| ------------ | ----- | ----- | ----- | +| en_US | zh_TW | ko_KR | ja_JP | + + + ## War Thunder ### 英文 News diff --git a/lib/v2/taptap/changelog.js b/lib/v2/taptap/changelog.js index 53ab97cf7..3e9318bf6 100644 --- a/lib/v2/taptap/changelog.js +++ b/lib/v2/taptap/changelog.js @@ -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, })), diff --git a/lib/v2/taptap/maintainer.js b/lib/v2/taptap/maintainer.js index b2caec287..e5dfc51a2 100644 --- a/lib/v2/taptap/maintainer.js +++ b/lib/v2/taptap/maintainer.js @@ -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'], }; diff --git a/lib/v2/taptap/radar.js b/lib/v2/taptap/radar.js index 869ab5375..075c54daf 100644 --- a/lib/v2/taptap/radar.js +++ b/lib/v2/taptap/radar.js @@ -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', + }, + ], + }, }; diff --git a/lib/v2/taptap/review.js b/lib/v2/taptap/review.js index c3c8c65fb..5a50a8c32 100644 --- a/lib/v2/taptap/review.js +++ b/lib/v2/taptap/review.js @@ -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, }; }; diff --git a/lib/v2/taptap/router.js b/lib/v2/taptap/router.js index 62e7fcdf4..9d21dcff0 100644 --- a/lib/v2/taptap/router.js +++ b/lib/v2/taptap/router.js @@ -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')); }; diff --git a/lib/v2/taptap/topic.js b/lib/v2/taptap/topic.js index dc489ffcf..c908bb865 100644 --- a/lib/v2/taptap/topic.js +++ b/lib/v2/taptap/topic.js @@ -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, diff --git a/lib/v2/taptap/utils.js b/lib/v2/taptap/utils.js index 20a33666e..fcdc02f44 100644 --- a/lib/v2/taptap/utils.js +++ b/lib/v2/taptap/utils.js @@ -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,