feat(route): support different languages for the kurogames (wuthering waves) route (#18781)

* feat: add wuthering waves route

* refactor: use arrow functions for better readability

* refactor: remove redundant nullish fallback

* refactor: use Promise.all(...) improve performance

* refactor: reduce complexity of limit parameter logic

* refactor: extract parse integer parameter to its own function

* refactor: add kuro games to namespace name

* refactor: make kurogames route for wuthering waves more flexible

This commit adds support for different languages.

* refactor: remove redundant wuthering waves route

* refactor: add language parameter description

* refactor: fallback to 30 feed items instead of unlimited

* refactor: rename language in description table

* refactor: filter articles beforehand and favour map over flatMap

* refactor: remove redundant code for article filtering
This commit is contained in:
Goestav
2025-04-14 15:30:19 +00:00
committed by GitHub
parent 6079e2e2b5
commit 6b302364e8
4 changed files with 174 additions and 34 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
import { Namespace } from '@/types';
export const namespace: Namespace = {
name: '库洛游戏',
name: '库洛游戏 | Kuro Games',
url: 'www.kurogames.com',
categories: ['game'],
lang: 'zh-CN',
@@ -0,0 +1,34 @@
/** The language. */
export enum Language {
English = 'en',
Japanese = 'jp',
Korean = 'kr',
/** Legacy code to ensure old results don't change. */
Chinese = 'zh',
ChineseTaiwan = 'zh-tw',
Spanish = 'es',
French = 'fr',
German = 'de',
}
/** Route parameters. */
export enum Parameter {
Limit = 'limit',
Language = 'language',
}
/** The languages supported by the API. */
export const SUPPORTED_LANGUAGES = Object.values(Language);
export interface Article {
articleContent: string;
articleDesc: string;
articleId: number;
articleTitle: string;
articleType: number;
createTime: string;
sortingMark: number;
startTime: string;
suggestCover: string;
top: number;
}
+60 -33
View File
@@ -4,56 +4,83 @@ import { parseDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
import * as cheerio from 'cheerio';
import ofetch from '@/utils/ofetch';
interface NewsItem {
articleContent: string;
articleDesc: string;
articleId: number;
articleTitle: string;
articleType: number;
createTime: string;
sortingMark: number;
startTime: string;
suggestCover: string;
top: number;
}
import { Article, Language, Parameter, SUPPORTED_LANGUAGES } from './constants';
import { fetchArticles, getArticleContentLink, getArticleLink, getHandlerLanguage, isValidLanguage, parseInteger } from './utils';
export const route: Route = {
path: '/wutheringwaves/news',
path: `/wutheringwaves/news/:${Parameter.Language}?`,
categories: ['game'],
example: '/kurogames/wutheringwaves/news',
parameters: {
[Parameter.Language]: 'The language to use for the content. Default: `zh`.',
},
name: '鸣潮 — 游戏公告、新闻与活动',
radar: [
{
source: ['mc.kurogames.com/m/main/news', 'mc.kurogames.com/main'],
},
{
title: 'Wuthering Waves — Game announcements, news and events',
source: ['wutheringwaves.kurogames.com/en/main/news', 'wutheringwaves.kurogames.com/en/main'],
},
],
maintainers: ['enpitsulin'],
description: '',
async handler() {
const res = await ofetch<NewsItem[]>('https://media-cdn-mingchao.kurogame.com/akiwebsite/website2.0/json/G152/zh/ArticleMenu.json', { query: { t: Date.now() } });
const item = await Promise.all(
res.map((i) => {
const contentUrl = `https://media-cdn-mingchao.kurogame.com/akiwebsite/website2.0/json/G152/zh/article/${i.articleId}.json`;
const item = {
title: i.articleTitle,
pubDate: timezone(parseDate(i.createTime), +8),
link: `https://mc.kurogames.com/main/news/detail/${i.articleId}`,
} as DataItem;
return cache.tryGet(contentUrl, async () => {
const data = await ofetch<NewsItem>(contentUrl, { query: { t: Date.now() } });
const $ = cheerio.load(data.articleContent);
maintainers: ['goestav', 'enpitsulin'],
description: `
Language codes for the \`${Parameter.Language}\` parameter:
| Language | Code |
|----------|--------------|
| English | en |
| 日本語 | jp |
| 한국어 | kr |
| 简体中文 | zh (default) |
| 繁體中文 | zh-tw |
| Español | es |
| Français | fr |
| Deutsch | de |
`,
async handler(ctx) {
const limitParam = ctx.req.query(Parameter.Limit);
const languageParam = ctx.req.param(Parameter.Language);
const limit = parseInteger(limitParam, 30);
const language = languageParam || Language.Chinese;
if (!isValidLanguage(language)) {
throw new TypeError(`Language parameter is not valid. Please use one of the following: ${SUPPORTED_LANGUAGES.join(', ')}`);
}
const articles = await fetchArticles(language);
const filteredArticles = articles.filter((a) => a.articleType !== 0).slice(0, limit);
const item = await Promise.all(
filteredArticles.map((article) => {
const contentUrl = getArticleContentLink(language, article.articleId);
const item: DataItem = {
title: article.articleTitle,
pubDate: timezone(parseDate(article.createTime), +8),
link: getArticleLink(language, article.articleId),
};
return cache.tryGet(`wutheringwaves:${language}:${article.articleId}`, async () => {
const { articleContent } = await ofetch<Article>(contentUrl, { query: { t: Date.now() } });
const $ = cheerio.load(articleContent);
item.description = $.html() ?? article.articleDesc ?? '';
item.description = $.html() ?? i.articleDesc ?? '';
return item;
}) as Promise<DataItem>;
})
);
const title = language === Language.Chinese ? '《鸣潮》— 游戏公告、新闻和活动' : 'Wuthering Waves - Announcements, News and Events';
const link = language === Language.Chinese ? 'https://mc.kurogames.com/main#news' : `https://wutheringwaves.kurogames.com/${language}/main/#news`;
return {
title: '《鸣潮》— 游戏公告、新闻和活动',
link: 'https://mc.kurogames.com/main#news',
title,
link,
item,
language: 'zh-cn',
language: getHandlerLanguage(language),
};
},
};
@@ -0,0 +1,79 @@
import ofetch from '@/utils/ofetch';
import { Data } from '@/types';
import { Article, Language, SUPPORTED_LANGUAGES } from './constants';
/**
* Parse a number or a number as string.\
* **NOTE:** this may return `NaN` if the string is not a number or the value is `undefined` and no {@link fallback} is provided.
*/
export const parseInteger = (value?: string | number, fallback?: number): number => {
if (typeof value === 'number') {
return value;
}
if (value === undefined) {
return fallback === undefined ? Number.NaN : fallback;
}
const parsed = Number.parseInt(value, 10);
if (fallback !== undefined && Number.isNaN(parsed)) {
return fallback;
}
return parsed;
};
/** Type-guard to ensure {@link language} is a valid value of {@link SUPPORTED_LANGUAGES}. */
export const isValidLanguage = (language: string): language is Language => SUPPORTED_LANGUAGES.includes(language as Language);
/** Fetch the articles for a given language in a given category. */
export const fetchArticles = (language: Language): Promise<Article[]> => {
if (language === Language.Chinese) {
return ofetch<Article[]>('https://media-cdn-mingchao.kurogame.com/akiwebsite/website2.0/json/G152/zh/ArticleMenu.json', { query: { t: Date.now() } });
}
return ofetch<{ article: Article[] }>(`https://hw-media-cdn-mingchao.kurogame.com/akiwebsite/website2.0/json/G152/${language}/MainMenu.json`).then((data) => data.article);
};
/** Get the link to the article content. */
export const getArticleContentLink = (language: Language, articleId: number): string => {
if (language === Language.Chinese) {
return `https://media-cdn-mingchao.kurogame.com/akiwebsite/website2.0/json/G152/zh/article/${articleId}.json`;
}
return `https://hw-media-cdn-mingchao.kurogame.com/akiwebsite/website2.0/json/G152/${language}/article/${articleId}.json`;
};
/** Get the link to an article from its ID. */
export const getArticleLink = (language: Language, articleId: number): string => {
if (language === Language.Chinese) {
return `https://mc.kurogames.com/main/news/detail/${articleId}`;
}
return `https://wutheringwaves.kurogames.com/${language}/main/news/detail/${articleId}`;
};
/** Resolve the handler language from the {@link Language}. */
export const getHandlerLanguage = (language: Language): Exclude<Data['language'], undefined> => {
switch (language) {
case Language.English:
return 'en';
case Language.Chinese:
return 'zh-CN';
case Language.ChineseTaiwan:
return 'zh-TW';
case Language.French:
return 'fr';
case Language.German:
return 'de';
case Language.Japanese:
return 'ja';
case Language.Korean:
return 'ko';
case Language.Spanish:
return 'es';
default:
throw new Error(`Could not resolve handler language from "${language}"`);
}
};