fix(route): taptap (#17732)

* fix(route): taptap

* fix(route): correct language code formatting in TapTap routes
This commit is contained in:
Tony 2024-11-27 13:03:03 -08:00 committed by GitHub
parent cc7062d697
commit 4b64e22c0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 722 additions and 290 deletions

View File

@ -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,
};

View File

@ -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 |`,
};

View File

@ -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,
})),
};
}

View File

@ -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,
})),
};
}

View File

@ -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,
};
}

View File

@ -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

View File

@ -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 |`,
};

View File

@ -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 |`,
};

View File

@ -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 |`,
};

View File

@ -1,3 +1,2 @@
{{ if intro }}{{@ intro }}{{ /if }}
<p>Preview</p>
<p><img src="{{@ previewUrl }}" /></p>

View File

@ -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;
}

363
lib/routes/taptap/types.ts Normal file
View File

@ -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;
}

View File

@ -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 };