diff --git a/lib/routes/taptap/changelog-cn.ts b/lib/routes/taptap/changelog-cn.ts new file mode 100644 index 00000000000000..52c9574b0347b5 --- /dev/null +++ b/lib/routes/taptap/changelog-cn.ts @@ -0,0 +1,29 @@ +import { Route } from '@/types'; +import { handler } from './common/changelog'; + +export const route: Route = { + path: '/changelog/:id/:lang?', + categories: ['game'], + example: '/taptap/changelog/60809/en_US', + parameters: { + id: '游戏 ID,游戏主页 URL 中获取', + lang: '语言,默认使用 `zh_CN`,亦可使用 `en_US`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.taptap.cn/app/:id'], + target: '/changelog/:id', + }, + ], + name: '游戏更新', + maintainers: ['hoilc', 'ETiV'], + handler, +}; diff --git a/lib/routes/taptap/changelog-intl.ts b/lib/routes/taptap/changelog-intl.ts new file mode 100644 index 00000000000000..e16f5fefa89b90 --- /dev/null +++ b/lib/routes/taptap/changelog-intl.ts @@ -0,0 +1,34 @@ +import { Route } from '@/types'; +import { handler } from './common/changelog'; + +export const route: Route = { + path: '/intl/changelog/:id/:lang?', + categories: ['game'], + example: '/taptap/intl/changelog/191001/zh_TW', + parameters: { + id: "Game's App ID, you may find it from the URL of the Game", + lang: 'Language, checkout the table below for possible values, default is `en_US`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.taptap.io/app/:id'], + target: '/intl/changelog/:id', + }, + ], + name: "Game's Changelog", + maintainers: ['hoilc', 'ETiV'], + handler, + description: `Language Code + + | English (US) | 繁體中文 | 한국어 | 日本語 | + | ------------ | -------- | ------ | ------ | + | en_US | zh_TW | ko_KR | ja_JP |`, +}; diff --git a/lib/routes/taptap/changelog.ts b/lib/routes/taptap/changelog.ts deleted file mode 100644 index 7f7d124da3c5f1..00000000000000 --- a/lib/routes/taptap/changelog.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import { parseDate } from '@/utils/parse-date'; -import { getRootUrl, appDetail, X_UA } from './utils'; - -export const route: Route = { - path: ['/changelog/:id/:lang?', '/intl/changelog/:id/:lang?'], - categories: ['game'], - example: '/taptap/changelog/60809/en_US', - parameters: { id: '游戏 ID,游戏主页 URL 中获取', lang: '语言,默认使用 `zh_CN`,亦可使用 `en_US`' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['taptap.com/app/:id'], - target: '/changelog/:id', - }, - ], - name: '游戏更新', - maintainers: ['hoilc', 'ETiV'], - handler, - description: `#### 语言代码 - - | English (US) | 繁體中文 | 한국어 | 日本語 | - | ------------ | -------- | ------ | ------ | - | en\_US | zh\_TW | ko\_KR | ja\_JP |`, -}; - -async function handler(ctx) { - const is_intl = ctx.req.url.indexOf('/intl/') === 0; - const id = ctx.req.param('id'); - const lang = ctx.req.param('lang') ?? (is_intl ? 'en_US' : 'zh_CN'); - - const url = `${getRootUrl(is_intl)}/app/${id}`; - - 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 app_description = `${app_name} by ${app_detail.app.developers.map((item) => item.name).join(' & ')}`; - - const response = await got({ - method: 'get', - url: `${getRootUrl(is_intl)}/webapiv2/apk/v1/list-by-app?app_id=${id}&from=0&limit=10&${X_UA(lang)}`, - headers: { - Referer: url, - }, - }); - - const list = response.data.data.list; - - return { - title: `TapTap 更新记录 ${app_name}`, - description: app_description, - link: url, - image: app_img, - item: list.map((item) => ({ - title: `${app_name} / ${item.version_label}`, - description: item.whatsnew.text, - pubDate: parseDate(item.update_date * 1000), - link: url, - guid: item.version_label, - })), - }; -} diff --git a/lib/routes/taptap/common/changelog.ts b/lib/routes/taptap/common/changelog.ts new file mode 100644 index 00000000000000..fd320e3a0a35bc --- /dev/null +++ b/lib/routes/taptap/common/changelog.ts @@ -0,0 +1,40 @@ +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { getRootUrl, appDetail, X_UA } from '../utils'; + +export async function handler(ctx) { + const requestPath = ctx.req.path.replace('/taptap', ''); + const isIntl = requestPath.startsWith('/intl/'); + const id = ctx.req.param('id'); + const lang = ctx.req.param('lang') ?? (isIntl ? 'en_US' : 'zh_CN'); + + const url = `${getRootUrl(isIntl)}/app/${id}`; + + const detail = await appDetail(id, lang, isIntl); + + const appImg = detail.app.icon.original_url; + const appName = detail.app.title; + const appDescription = `${appName}${detail.app.developers ? ' by' + detail.app.developers.map((item) => item.name).join(' & ') : ''}`; + + const response = await ofetch(`${getRootUrl(isIntl)}/webapiv2/apk/v1/list-by-app?app_id=${id}&from=0&limit=10&${X_UA(lang)}`, { + headers: { + Referer: url, + }, + }); + + const list = response.data.list; + + return { + title: `TapTap 更新记录 ${appName}`, + description: appDescription, + link: url, + image: appImg, + item: list.map((item) => ({ + title: `${appName} / ${item.version_label}`, + description: item.whatsnew.text, + pubDate: parseDate(item.update_date, 'X'), + link: url, + guid: item.version_label, + })), + }; +} diff --git a/lib/routes/taptap/common/review.ts b/lib/routes/taptap/common/review.ts new file mode 100644 index 00000000000000..80bb0602e6e994 --- /dev/null +++ b/lib/routes/taptap/common/review.ts @@ -0,0 +1,126 @@ +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { getRootUrl, appDetail, X_UA } from '../utils'; + +/* +const sortMap = { + default: { + en_US: 'Default', + zh_CN: '预设', + zh_TW: '預設', + }, + recent: { + en_US: 'Latest', + zh_CN: '最新', + zh_TW: '最新', + }, + hot: { + en_US: 'Popular', + zh_CN: '热门', + zh_TW: '熱門', + }, + spent: { + en_US: 'Play Time', + zh_CN: '游戏时长', + zh_TW: '遊戲時長', + }, +}; + +const intlSortMap = { + helpful: { + en_US: 'Most Helpful', + zh_TW: '最有幫助', + ja_JP: '最も役立つ', + ko_KR: '가장 도움이 된', + }, + recent: { + en_US: 'Most Recent', + zh_TW: '最新', + ja_JP: '最も最近', + ko_KR: '최근순', + }, +}; +*/ + +const makeSortParam = (isIntl: boolean, order: string) => { + if (isIntl) { + if (order === 'helpful' || order === 'recent') { + return `type=${order}`; + } + return 'type=helpful'; + } else { + if (order === 'new' || order === 'hot') { + return `sort=${order}`; + } + return 'sort=hot'; + } +}; + +const fetchMainlandItems = async (params) => { + const id = params.id; + const order = params.order ?? 'hot'; + const lang = params.lang ?? 'zh_CN'; + + let url = `${getRootUrl(false)}/webapiv2/review/v2/list-by-app?app_id=${id}&limit=10`; + url += `&${makeSortParam(false, order)}`; + url += `&${X_UA(lang)}`; + + const reviewListResponse = await ofetch(url); + + return reviewListResponse.data.list.map((review) => { + const author = review.moment.author.user.name; + const score = review.moment.review.score; + return { + title: `${author} - ${score}星`, + author, + description: review.moment.review.contents.text + (review.moment.review.contents.images ? review.moment.review.contents.images.map((img) => ``).join('') : ''), + link: `${getRootUrl(false)}/review/${review.moment.review.id}`, + pubDate: parseDate(review.moment.publish_time, 'X'), + }; + }); +}; + +const fetchIntlItems = async (params) => { + const id = params.id; + const order = params.order ?? 'helpful'; + const lang = params.lang ?? 'en_US'; + + let url = `${getRootUrl(true)}/webapiv2/feeds/v3/by-app?app_id=${id}&limit=10`; + url += `&${makeSortParam(true, order)}`; + url += `&${X_UA(lang)}`; + + const reviewListResponse = await ofetch(url); + + return reviewListResponse.data.list.map((review) => { + const author = review.post.user.name; + const score = review.post.list_fields.app_ratings[id].score; + return { + title: `${author} - ${'★'.repeat(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, 'X'), + }; + }); +}; + +export async function handler(ctx) { + const requestPath = ctx.req.path.replace('/taptap', ''); + const isIntl = requestPath.startsWith('/intl/'); + const id = ctx.req.param('id'); + const order = ctx.req.param('order') ?? 'default'; + const lang = ctx.req.param('lang') ?? (isIntl ? 'en_US' : 'zh_CN'); + + const detail = await appDetail(id, lang, isIntl); + const appImg = detail.app.icon.original_url; + const appName = detail.app.title; + + const items = isIntl ? await fetchIntlItems({ id, order, lang }) : await fetchMainlandItems({ id, order, lang }); + + return { + title: `TapTap 评价 ${appName}`, + link: `${getRootUrl(isIntl)}/app/${id}/review?${makeSortParam(isIntl, order)}`, + image: appImg, + item: items, + }; +} diff --git a/lib/routes/taptap/namespace.ts b/lib/routes/taptap/namespace.ts index c13466ece33949..6af928b840b5fa 100644 --- a/lib/routes/taptap/namespace.ts +++ b/lib/routes/taptap/namespace.ts @@ -1,8 +1,8 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { - name: 'TapTap 中国', - url: 'taptap.com', + name: 'TapTap', + url: 'www.taptap.io', description: `:::warning 由于区域限制,需要在有国内 IP 的机器上自建才能正常获取 RSS。\ 而对于《TapTap 国际版》则需要部署在具有海外出口的 IP 上才可正常获取 RSS。 diff --git a/lib/routes/taptap/review-cn.ts b/lib/routes/taptap/review-cn.ts new file mode 100644 index 00000000000000..dc38b771a21d1c --- /dev/null +++ b/lib/routes/taptap/review-cn.ts @@ -0,0 +1,33 @@ +import { Route } from '@/types'; +import { handler } from './common/review'; + +export const route: Route = { + path: '/review/:id/:order?/:lang?', + categories: ['game'], + example: '/taptap/review/142793/hot', + parameters: { + id: '游戏 ID,游戏主页 URL 中获取', + order: '排序方式,空为综合,可选如下', + lang: '语言,`zh-CN` 或 `zh-TW`,默认为 `zh-CN`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.taptap.cn/app/:id/review', 'www.taptap.cn/app/:id'], + target: '/review/:id', + }, + ], + name: '游戏评价', + maintainers: ['hoilc', 'TonyRL'], + handler, + description: `| 最新 | 综合 | +| --- | --- | +| new | hot |`, +}; diff --git a/lib/routes/taptap/review-intl.ts b/lib/routes/taptap/review-intl.ts new file mode 100644 index 00000000000000..88936dec4a63a1 --- /dev/null +++ b/lib/routes/taptap/review-intl.ts @@ -0,0 +1,41 @@ +import { Route } from '@/types'; +import { handler } from './common/review'; + +export const route: Route = { + path: '/intl/review/:id/:order?/:lang?', + categories: ['game'], + example: '/taptap/intl/review/82354/recent', + parameters: { + id: "Game's App ID, you may find it from the URL of the Game", + order: 'Sort Method, default is `helpful`, checkout the table below for possible values', + lang: 'Language, checkout the table below for possible values, default is `en_US`', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.taptap.io/app/:id/review', 'www.taptap.io/app/:id'], + target: '/intl/review/:id', + }, + ], + name: 'Ratings & Reviews', + maintainers: ['hoilc', 'TonyRL', 'ETiV'], + handler, + description: `Sort Method + +| Most Helpful | Most Recent | +| ------------ | ----------- | +| helpful | recent | + +Language Code + +| English (US) | 繁體中文 | 한국어 | 日本語 | +| ------------ | -------- | ------ | ------ | +| en_US | zh_TW | ko_KR | ja_JP |`, +}; diff --git a/lib/routes/taptap/review.ts b/lib/routes/taptap/review.ts deleted file mode 100644 index ac3568e4ba6589..00000000000000 --- a/lib/routes/taptap/review.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import { parseDate } from '@/utils/parse-date'; -import { getRootUrl, appDetail, X_UA } from './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: '遊戲時長', - }, -}; - -const intlSortMap = { - default: { - en_US: 'Most Relevant', - zh_TW: '最相關', - ja_JP: '関連性が高い', - ko_KR: '관련도순', - }, - new: { - en_US: 'Most Recent', - zh_TW: '最新', - ja_JP: '最も最近', - ko_KR: '최근순', - }, -}; - -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.data.list; - - 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: `${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), - }; - }); -}; - -async function handler(ctx) { - const is_intl = ctx.req.url.indexOf('/intl/') === 0; - const id = ctx.req.param('id'); - const order = ctx.req.param('order') ?? 'default'; - const lang = ctx.req.param('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); - - const ret = { - 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.set('json', ret); - return ret; -} - -export const route: Route = { - path: ['/review/:id/:order?/:lang?', '/intl/review/:id/:order?/:lang?'], - categories: ['game'], - example: '/taptap/review/142793/hot', - parameters: { id: '游戏 ID,游戏主页 URL 中获取', order: '排序方式,空为默认排序,可选如下', lang: '语言,`zh-CN`或`zh-TW`,默认为`zh-CN`' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['taptap.com/app/:id/review', 'taptap.com/app/:id'], - target: '/review/:id', - }, - ], - name: '游戏评价', - maintainers: ['hoilc', 'TonyRL'], - handler, - description: `#### 排序方式 - - | 最相关 | 最新 | - | ------- | ---- | - | default | new | - - #### 语言代码 - - | English (US) | 繁體中文 | 한국어 | 日本語 | - | ------------ | -------- | ------ | ------ | - | en\_US | zh\_TW | ko\_KR | ja\_JP |`, - description: `| 最新 | 最热 | 游戏时长 | 默认排序 | - | ------ | ---- | -------- | -------- | - | update | hot | spent | default |`, -}; diff --git a/lib/routes/taptap/templates/videoPost.art b/lib/routes/taptap/templates/videoPost.art index 184bcecb2bebf2..89d092cdf86487 100644 --- a/lib/routes/taptap/templates/videoPost.art +++ b/lib/routes/taptap/templates/videoPost.art @@ -1,3 +1,2 @@ -{{ if intro }}{{@ intro }}{{ /if }}

Preview

diff --git a/lib/routes/taptap/topic.ts b/lib/routes/taptap/topic.ts index 5df3417e78e0b4..f99216b3870b1d 100644 --- a/lib/routes/taptap/topic.ts +++ b/lib/routes/taptap/topic.ts @@ -1,6 +1,6 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import { getRootUrl, X_UA, appDetail, imagePost, topicPost, videoPost } from './utils'; @@ -27,7 +27,12 @@ export const route: Route = { path: '/topic/:id/:type?/:sort?/:lang?', categories: ['game'], example: '/taptap/topic/142793/official', - parameters: { id: '游戏 ID,游戏主页 URL 中获取', type: '论坛版块,默认显示所有帖子,论坛版块 URL 中 `type` 参数,见下表,默认为 `feed`', sort: '排序,见下表,默认为 `created`', lang: '语言,`zh-CN`或`zh-TW`,默认为`zh-CN`' }, + parameters: { + id: '游戏 ID,游戏主页 URL 中获取', + type: '论坛版块,默认显示所有帖子,论坛版块 URL 中 `type` 参数,见下表,默认为 `feed`', + sort: '排序,见下表,默认为 `created`', + lang: '语言,`zh-CN`或`zh-TW`,默认为`zh-CN`', + }, features: { requireConfig: false, requirePuppeteer: false, @@ -38,7 +43,7 @@ export const route: Route = { }, radar: [ { - source: ['taptap.com/app/:id/topic', 'taptap.com/app/:id'], + source: ['taptap.cn/app/:id/topic', 'taptap.cn/app/:id'], target: '/topic/:id', }, ], @@ -46,12 +51,12 @@ export const route: Route = { maintainers: ['hoilc', 'TonyRL'], handler, description: `| 全部 | 精华 | 官方 | 影片 | - | ---- | ----- | -------- | ----- | - | feed | elite | official | video | +| ---- | ----- | -------- | ----- | +| feed | elite | official | video | - | 发布时间 | 回复时间 | - | -------- | --------- | - | created | commented |`, +| 发布时间 | 回复时间 | 默认排序 | +| -------- | --------- | ------- | +| created | commented | default |`, }; async function handler(ctx) { @@ -61,31 +66,30 @@ async function handler(ctx) { const type = ctx.req.param('type') ?? 'feed'; const sort = ctx.req.param('sort') ?? 'created'; 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 = `${getRootUrl(false)}/webapiv2/feed/v6/by-group?group_id=${groupId}&type=${type}&sort=${sort}&${X_UA(lang)}`; + const appImg = appData.app.icon.original_url || appData.app.icon.url; + const appName = appData.app.title; + const url = `${getRootUrl(false)}/webapiv2/feed/v7/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; + const topicsList = await ofetch(url); + const list = topicsList.data.list; const out = await Promise.all( - topics_list.data.list.map((list) => { - const link = list.moment.sharing?.url || list.moment.extended_entities?.topics?.[0].sharing.url || list.moment.extended_entities?.videos?.[0].sharing.url; + list.map(({ moment }) => { + const link = moment.sharing.url; return cache.tryGet(link, async () => { - const author = list.moment.author.user.name; - const topicId = list.moment.extended_entities?.topics?.[0].id; + const author = moment.author.user.name; + const topicId = moment.topic.id_str; // raw_text sometimes is "" so || is better than ?? - const title = list.moment.contents?.raw_text || list.moment.extended_entities?.topics?.[0].title || list.moment.extended_entities?.videos?.[0].title; - const createdTime = list.moment.created_time; - let description = ''; - if (topicId) { - description = await topicPost(appId, topicId, lang); - } else { - description = list.moment.extended_entities?.topics?.[0].summary || list.moment.contents?.raw_text || videoPost(list.moment.extended_entities?.videos?.[0]); - if (list.moment.extended_entities?.images) { - description += imagePost(list.moment.extended_entities.images); + const title = moment.topic.title || moment.topic.summary.split(' ')[0]; + let description = moment.topic.summary || ''; + if (moment.topic.pin_video) { + description += videoPost(moment.topic.pin_video); + if (moment.topic.footer_images?.images) { + description += imagePost(moment.topic.footer_images.images); } + } else { + description = await topicPost(appId, topicId, lang); } return { @@ -93,16 +97,16 @@ async function handler(ctx) { description, author, link, - pubDate: parseDate(createdTime, 'X'), + pubDate: parseDate(moment.created_time, 'X'), }; }); }) ); const ret = { - title: `${app_name} - ${typeMap[type][lang]} - TapTap 论坛`, + title: `${appName} - ${typeMap[type][lang]} - TapTap 论坛`, link: `${getRootUrl(false)}/app/${appId}/topic?type=${type}&sort=${sort}`, - image: app_img, + image: appImg, item: out.filter((item) => item !== ''), }; @@ -110,7 +114,7 @@ async function handler(ctx) { ...ret, appId, groupId, - topics_list, + topicsList, }); return ret; } diff --git a/lib/routes/taptap/types.ts b/lib/routes/taptap/types.ts new file mode 100644 index 00000000000000..d8f35e3c4e7e55 --- /dev/null +++ b/lib/routes/taptap/types.ts @@ -0,0 +1,363 @@ +export interface Detail { + group: Group; + app: App; +} + +interface Group { + id: number; + app_id: number; + title: string; + intro: string; + has_treasure: boolean; + has_official: boolean; + icon: Icon; + banner: Banner; + moderators: Moderator[]; + log: Log; + event_log: EventLog; + stat: Stat; + web_url: string; + style_info: StyleInfo; + terms: Term[]; + sharing: Sharing; + new_rec_list: NewRecListItem[]; + actions: Actions; + title_labels: TitleLabel[]; +} + +interface Icon { + url: string; + medium_url: string; + small_url: string; + original_url: string; + original_format: string; + width: number; + height: number; + color: string; + original_size: number; +} + +interface Banner { + url: string; + medium_url: string; + small_url: string; + original_url: string; + original_format: string; + width: number; + height: number; + color: string; + original_size: number; +} + +interface Moderator { + id: number; + name: string; + avatar: string; + medium_avatar: string; + avatar_pendant: string; + badges: Badge[]; + verified: Verified; +} + +interface Badge { + id: number; + title: string; + description: string; + is_wear: boolean; + icon: BadgeIcon; + style: BadgeStyle; + time: number; + unlock_tips: string; + level: number; + status: number; +} + +interface BadgeIcon { + small: string; + middle: string; + large: string; + small_border: string; +} + +interface BadgeStyle { + background_image: string; + background_color: string; + font_color: string; + border_background_color: string; +} + +interface Verified { + type: string; + reason: string; + url: string; +} + +interface Log { + follow: Follow; + unfollow: Follow; +} + +interface Follow { + uri: string; + params: FollowParams; +} + +interface FollowParams { + type: string; + APIVersion: string; + paramId: string; + paramType: string; +} + +interface EventLog { + paramType: string; + paramId: string; +} + +interface Stat { + favorite_count: number; + topic_count: number; + elite_topic_count: number; + recent_topic_count: number; + official_topic_count: number; + top_topic_count: number; + video_count: number; + user_moment_count: number; + app_moment_count: number; + image_moment_count: number; + treasure_count: number; + topic_page_view: number; + feed_count: number; + topic_pv_total: number; +} + +interface StyleInfo { + background_color: string; + font_color: string; +} + +interface Term { + label: string; + type: string; + index: string; + params: TermParams; + sort: TermSort[]; + log: TermLog; + position: string; + referer_ext: string; + log_keyword: string; + top_params: TermParams; + sub_terms: SubTerm[]; +} + +interface TermParams { + type: string; +} + +interface TermSort { + label: string; + params: SortParams; + icon_type: string; +} + +interface SortParams { + sort: string; +} + +interface TermLog { + page_view: PageView; +} + +interface PageView { + uri: string; + params: PageViewParams; +} + +interface PageViewParams { + type: string; + APIVersion: string; + paramId: string; + paramType: string; + position: string; +} + +interface SubTerm { + label: string; + type: string; + index: string; + params: SubTermParams; + top_params: SubTermTopParams; + sort: SubTermSort[]; + log: SubTermLog; + referer_ext: string; + log_keyword: string; + uri: string; + web_url: string; +} + +interface SubTermParams { + group_label_id: string; + type: string; +} + +interface SubTermTopParams { + group_label_id: string; + is_top: string; + type: string; +} + +interface SubTermSort { + label: string; + params: SortParams; + icon_type: string; +} + +interface SubTermLog { + page_view: PageView; +} + +interface Sharing { + url: string; + title: string; + description: string; + image: Icon; +} + +interface NewRecListItem { + icon: Icon; + uri: string; + url: string; + web_url: string; + label: string; +} + +interface Actions { + publish_contents: boolean; +} + +interface TitleLabel { + label: string; + icon: Icon; +} + +interface App { + id: number; + identifier: string; + title: string; + title_labels: string[]; + icon: Icon; + price: Price; + uri: Uri; + can_view: boolean; + released_time: number; + button_flag: number; + style: number; + hidden_button: boolean; + is_deny_minors: boolean; + stat: AppStat; + ad_banner: Icon; + top_banner: Icon; + banner: Icon; + tags: Tag[]; + log: AppLog; + event_log: EventLog; + can_buy_redeem_code: CanBuyRedeemCode; + show_module: ShowModule[]; + complaint: Complaint; + serial_number: SerialNumber; + rec_text: string; + readable_id: string; + m_button_map: object; + description: Description; + title_labels_v2: TitleLabel[]; + stat_key: string; + include_app_product_type_complete: boolean; + is_console_game: boolean; +} + +interface Price { + taptap_current: string; + discount_rate: number; +} + +interface Uri { + google: string; + google_play: string; + apple: string; + download_site: string; +} + +interface AppStat { + rating: Rating; + vote_info: VoteInfo; + hits_total: number; + play_total: number; + bought_count: number; + feed_count: number; + reserve_count: number; + recent_sandbox_played_count: number; + album_count: number; + review_count: number; + topic_count: number; + video_count: number; + official_topic_count: number; + official_video_count: number; + official_album_count: number; + fans_count: number; +} + +interface Rating { + score: string; + max: number; + latest_score: string; + latest_version_score: string; +} + +interface VoteInfo { + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; +} + +interface Tag { + id: number; + value: string; + uri: string; + web_url: string; +} + +interface AppLog { + follow: Follow; + open: Follow; + page_view: Follow; + play: Follow; + reserve: Follow; + unfollow: Follow; + unreserved: Follow; +} + +interface CanBuyRedeemCode { + flag: boolean; +} + +interface ShowModule { + key: string; + value: boolean; +} + +interface Complaint { + uri: string; + web_url: string; + url: string; +} + +interface SerialNumber { + number_exists: boolean; + button_action: number; +} + +interface Description { + text: string; +} diff --git a/lib/routes/taptap/utils.ts b/lib/routes/taptap/utils.ts index 9c7ff36f88471d..602e91963ada63 100644 --- a/lib/routes/taptap/utils.ts +++ b/lib/routes/taptap/utils.ts @@ -1,24 +1,26 @@ import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import path from 'node:path'; import { art } from '@/utils/render'; +import cache from '@/utils/cache'; +import { Detail } from './types'; -// 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 X_UA = (lang = 'zh_CN') => `X-UA=${encodeURIComponent(`V=1&PN=WebApp&VN=0.1.0&LANG=${lang}&PLT=PC`)}`; -const getRootUrl = (isIntl = false) => (isIntl ? 'https://www.taptap.io' : 'https://www.taptap.com'); +const getRootUrl = (isIntl = false) => (isIntl ? 'https://www.taptap.io' : 'https://www.taptap.cn'); -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: `${getRootUrl(isIntl)}/app/${appId}`, - }, - }); - return data.data; -}; +const appDetail = (appId, lang = 'zh_CN', isIntl = false) => + cache.tryGet(`taptap:appDetail:${appId}:${lang}:${isIntl}`, async () => { + const data = await ofetch(`${getRootUrl(isIntl)}/webapiv2/group/v1/detail?app_id=${appId}&${X_UA(lang)}`, { + headers: { + Referer: `${getRootUrl(isIntl)}/app/${appId}`, + }, + }); + return data.data; + }) as Promise; const imagePost = (images) => art(path.join(__dirname, 'templates/imagePost.art'), { @@ -26,25 +28,23 @@ const imagePost = (images) => }); const topicPost = async (appId, topicId, lang = 'zh_CN') => { - const res = await got(`${getRootUrl(false)}/webapiv2/topic/v1/detail?id=${topicId}&${X_UA(lang)}`, { + const res = await ofetch(`${getRootUrl(false)}/webapiv2/topic/v1/detail?id=${topicId}&${X_UA(lang)}`, { headers: { Referer: `${getRootUrl(false)}/app/${appId}`, }, }); - const $ = load(res.data.data.first_post.contents.text, null, false); + const $ = load(res.data.first_post.contents.text, null, false); $('img').each((_, e) => { - e = $(e); - e.attr('src', e.attr('data-origin-url')); - e.attr('referrerpolicy', 'no-referrer'); - e.removeAttr('data-origin-url'); + const $e = $(e); + $e.attr('src', $e.attr('data-origin-url')); + $e.removeAttr('data-origin-url'); }); return $.html(); }; const videoPost = (video) => art(path.join(__dirname, 'templates/videoPost.art'), { - intro: video?.intro?.text, - previewUrl: video?.video_resource.preview_animation.original_url, + previewUrl: video.thumbnail.original_url || video.thumbnail.url, }); export { getRootUrl, X_UA, appDetail, imagePost, topicPost, videoPost };