feat(route/hupu): news route for teams

This commit is contained in:
Stephen Zhou
2025-12-05 23:09:12 +08:00
parent a7ef6b6c91
commit 601ce1a72e
5 changed files with 1720 additions and 83 deletions
+46
View File
@@ -0,0 +1,46 @@
// https://games.mobileapi.hupu.com/3/8.2.30/basketballapi/teamStandingList?offline=json&competitionLeagueType=nba&competitionType=nba&season=2025-2026&competitionStageType=REGULAR&client=EB5576BA-C415-43F7-850F-11B13E7991EB
import { result } from './response/teamStandingList.json';
const NBA_TEAMS = [...result.rankTypeListMap.E, ...result.rankTypeListMap.W];
const NBA_TEAM_NAMES: Record<string, string> = {
活塞: 'Pistons',
尼克斯: 'Knicks',
猛龙: 'Raptors',
热火: 'Heat',
凯尔特人: 'Celtics',
魔术: 'Magic',
'76人': '76ers',
骑士: 'Cavaliers',
老鹰: 'Hawks',
雄鹿: 'Bucks',
公牛: 'Bulls',
黄蜂: 'Hornets',
篮网: 'Nets',
步行者: 'Pacers',
奇才: 'Wizards',
雷霆: 'Thunder',
湖人: 'Lakers',
火箭: 'Rockets',
马刺: 'Spurs',
掘金: 'Nuggets',
森林狼: 'Timberwolves',
太阳: 'Suns',
勇士: 'Warriors',
灰熊: 'Grizzlies',
开拓者: 'Trail Blazers',
爵士: 'Jazz',
独行侠: 'Mavericks',
快船: 'Clippers',
国王: 'Kings',
鹈鹕: 'Pelicans',
};
export const NBA_TEAMS_ID_MAP = NBA_TEAMS.reduce(
(map, team) => {
const englishName = NBA_TEAM_NAMES[team.teamName];
map[englishName.toLowerCase()] = team;
return map;
},
{} as Record<string, (typeof NBA_TEAMS)[number] | undefined>
);
+3 -83
View File
@@ -1,13 +1,11 @@
import { load } from 'cheerio';
import type { Data, DataItem, Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate, parseRelativeDate } from '@/utils/parse-date';
import { parseDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
import type { HomePostItem, HupuApiResponse, NewsDataItem } from './types';
import { isHomePostItem } from './types';
import { getEntryDetails } from './utils';
const categories = {
nba: {
@@ -112,85 +110,7 @@ export const route: Route = {
} satisfies DataItem)
);
items = await Promise.all(
items
.filter((item) => item.link && !/subject/.test(item.link))
.map((item) =>
cache.tryGet(item.link!, async () => {
try {
const detailResponse = await got({
method: 'get',
url: item.link,
});
const content = load(detailResponse.data);
const author = content('.bbs-user-info-name, .bbs-user-wrapper-content-name-span').text();
const pubDateString = content('.second-line-user-info span:not([class])').text();
// Possible formats: 10:21, 45分钟前, 09-15 19:57
const currentYear = new Date().getFullYear();
const currentDate = new Date();
const monthDayTimePattern = /^(\d{2})-(\d{2}) (\d{2}):(\d{2})$/;
const timeOnlyPattern = /^(\d{1,2}):(\d{2})$/;
let processedDateString = pubDateString;
if (monthDayTimePattern.test(pubDateString)) {
processedDateString = `${currentYear}-${pubDateString}`;
} else if (timeOnlyPattern.test(pubDateString)) {
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const day = String(currentDate.getDate()).padStart(2, '0');
processedDateString = `${currentYear}-${month}-${day} ${pubDateString}`;
}
const pubDate = [item.pubDate, timezone(parseDate(processedDateString), +8), timezone(parseRelativeDate(pubDateString), +8)].find((d) => d instanceof Date && !Number.isNaN(d.getTime()));
const categories = content('.basketballTobbs_tag > a, .tag-player-team')
.toArray()
.map((c) => content(c).text())
.filter(Boolean);
content('.basketballTobbs_tag').remove();
content('.hupu-img').each(function () {
const imgSrc = content(this).attr('data-gif') || content(this).attr('data-origin') || content(this).attr('src');
content(this).parent().html(`<img src="${imgSrc}">`);
});
// 分别获取内容元素
const descriptionParts: string[] = [];
// 获取主要内容
const mainContent = content('#bbs-thread-content, .bbs-content-font').html();
if (mainContent) {
descriptionParts.push(mainContent);
}
// 单独处理视频部分
const videoWrapper = content('.header-video-wrapper');
if (videoWrapper.length > 0) {
const videoElement = videoWrapper.find('video');
if (videoElement.length > 0) {
const videoHtml = videoElement.prop('outerHTML');
if (videoHtml) {
descriptionParts.push(videoHtml);
}
}
}
const description = descriptionParts.length > 0 ? descriptionParts.join('') : undefined;
return {
...item,
author,
category: categories.length > 0 ? categories : item.category,
description,
pubDate,
};
} catch {
// no-empty
return item;
}
})
)
);
items = await Promise.all(items.filter((item) => item.link && !/subject/.test(item.link)).map((item) => getEntryDetails(item)));
return {
title: `虎扑 - ${categories[category].title}`,
+44
View File
@@ -0,0 +1,44 @@
import type { Data, DataItem, Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
import { NBA_TEAMS_ID_MAP } from './consts';
import { getEntryDetails } from './utils';
export const route: Route = {
path: ['/news/:team'],
name: '队伍新闻',
url: 'm.hupu.com',
maintainers: ['hyoban'],
example: '/news/Spurs',
parameters: {
team: {
description: '全小写的英文队名,例如:spurs, lakers, warriors 等等',
},
},
categories: ['bbs'],
handler: async (ctx): Promise<Data> => {
const team = NBA_TEAMS_ID_MAP[ctx.req.param('team')];
const teamId = team?.teamId;
if (!teamId) {
throw new Error('Invalid team name');
}
const data = await ofetch(`https://games.mobileapi.hupu.com/3/7.5.60/basketballapi/news/v2/teamNewsById?cateGoryCode=basketball&clientId=93977196&newsId=0&teamId=${teamId}`);
let items: DataItem[] = data.result.map((item) => ({
title: item.title,
guid: item.tid,
link: `https://m.hupu.com/bbs/${item.tid}`,
pubDate: timezone(parseDate(item.publishTime), +8),
}));
items = await Promise.all(items.map((item) => getEntryDetails(item)));
return {
title: `虎扑 - ${team.teamName} 新闻`,
link: 'https://m.hupu.com',
item: items,
} as Data;
},
};
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
import { load } from 'cheerio';
import type { DataItem } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate, parseRelativeDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
export function getEntryDetails(item: DataItem): Promise<DataItem> {
if (!item.link) {
return Promise.resolve(item);
}
return cache.tryGet(item.link!, async () => {
try {
const detailResponse = await got({
method: 'get',
url: item.link,
});
const content = load(detailResponse.data);
const author = content('.bbs-user-info-name, .bbs-user-wrapper-content-name-span').text();
const pubDateString = content('.second-line-user-info span:not([class])').text();
// Possible formats: 10:21, 45分钟前, 09-15 19:57
const currentYear = new Date().getFullYear();
const currentDate = new Date();
const monthDayTimePattern = /^(\d{2})-(\d{2}) (\d{2}):(\d{2})$/;
const timeOnlyPattern = /^(\d{1,2}):(\d{2})$/;
let processedDateString = pubDateString;
if (monthDayTimePattern.test(pubDateString)) {
processedDateString = `${currentYear}-${pubDateString}`;
} else if (timeOnlyPattern.test(pubDateString)) {
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const day = String(currentDate.getDate()).padStart(2, '0');
processedDateString = `${currentYear}-${month}-${day} ${pubDateString}`;
}
const pubDate = [item.pubDate, timezone(parseDate(processedDateString), +8), timezone(parseRelativeDate(pubDateString), +8)].find((d) => d instanceof Date && !Number.isNaN(d.getTime()));
const categories = content('.basketballTobbs_tag > a, .tag-player-team')
.toArray()
.map((c) => content(c).text())
.filter(Boolean);
content('.basketballTobbs_tag').remove();
content('.hupu-img').each(function () {
const imgSrc = content(this).attr('data-gif') || content(this).attr('data-origin') || content(this).attr('src');
content(this).parent().html(`<img src="${imgSrc}">`);
});
// 分别获取内容元素
const descriptionParts: string[] = [];
// 获取主要内容
const mainContent = content('#bbs-thread-content, .bbs-content-font').html();
if (mainContent) {
descriptionParts.push(mainContent);
}
// 单独处理视频部分
const videoWrapper = content('.header-video-wrapper');
if (videoWrapper.length > 0) {
const videoElement = videoWrapper.find('video');
if (videoElement.length > 0) {
const videoHtml = videoElement.prop('outerHTML');
if (videoHtml) {
descriptionParts.push(videoHtml);
}
}
}
const description = descriptionParts.length > 0 ? descriptionParts.join('') : undefined;
return {
...item,
author,
category: categories.length > 0 ? categories : item.category,
description,
pubDate,
};
} catch {
// no-empty
return item;
}
});
}