fix(route): taptap (#17732)
* fix(route): taptap * fix(route): correct language code formatting in TapTap routes
This commit is contained in:
parent
cc7062d697
commit
4b64e22c0f
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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 |`,
|
||||
};
|
||||
|
|
@ -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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
@ -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) => `<img src="${img.original_url}">`).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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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。
|
||||
|
|
|
|||
|
|
@ -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 |`,
|
||||
};
|
||||
|
|
@ -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 |`,
|
||||
};
|
||||
|
|
@ -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 |`,
|
||||
};
|
||||
|
|
@ -1,3 +1,2 @@
|
|||
{{ if intro }}{{@ intro }}{{ /if }}
|
||||
<p>Preview</p>
|
||||
<p><img src="{{@ previewUrl }}" /></p>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<Detail>;
|
||||
|
||||
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 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue