feat: 接入BangDream查歌插件
This commit is contained in:
parent
f74bd14c34
commit
9d029ecb6a
@ -516,7 +516,8 @@ ON DUPLICATE KEY UPDATE
|
||||
INSERT INTO `qqbot_command` (`id`, `code`, `name`, `aliases`, `prefixes`, `plugin_key`, `operation_key`, `parser_key`, `target_type`, `default_params`, `reply_template`, `error_template`, `enabled`, `priority`, `cooldown_ms`, `remark`)
|
||||
VALUES
|
||||
(2041700000000300501, 'ff14_price', 'FF14 查价', '["查价","price","ff14price"]', '["/","!","!"]', 'ff14Market', 'ff14.market.price', 'ff14Price', 'all', '{"language":"chs","world":"中国"}', '', 'FF14 查价失败:{{error}}', 1, 0, 1500, '默认示例命令;请在账号配置中绑定后启用'),
|
||||
(2041700000000300502, 'fflogs_character', 'FFLogs 查询', '["fflogs","logs","查logs","查log"]', '["/","!","!"]', 'fflogs', 'fflogs.character.summary', 'fflogsCharacter', 'all', '{"serverRegion":"CN"}', '', 'FFLogs 查询失败:{{error}}', 1, 0, 3000, '查询 FFLogs 角色公开排名;带高难任务时返回最近10次记录;格式:/fflogs 角色名 服务器 [高难任务]')
|
||||
(2041700000000300502, 'fflogs_character', 'FFLogs 查询', '["fflogs","logs","查logs","查log"]', '["/","!","!"]', 'fflogs', 'fflogs.character.summary', 'fflogsCharacter', 'all', '{"serverRegion":"CN"}', '', 'FFLogs 查询失败:{{error}}', 1, 0, 3000, '查询 FFLogs 角色公开排名;带高难任务时返回最近10次记录;格式:/fflogs 角色名 服务器 [高难任务]'),
|
||||
(2041700000000300503, 'bangdream_song', 'BangDream 查歌', '["bd","bangdream","bandori","邦邦","邦邦查歌"]', '["/","!","!"]', 'bangDream', 'bangdream.song.search', 'plain', 'all', '{}', '', 'BangDream 查询失败:{{error}}', 1, 0, 1500, '查询 BanG Dream 歌曲信息;格式:/bd 歌曲名 或 /bd 歌曲ID')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name` = VALUES(`name`),
|
||||
`plugin_key` = VALUES(`plugin_key`),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { formatKtDateTime, throwVbenError } from '@/common';
|
||||
import { QqbotBangDreamPluginService } from '../plugins/bangDream/qqbot-bangdream.plugin';
|
||||
import { QqbotFf14MarketPluginService } from '../plugins/ff14Market/qqbot-ff14-market.plugin';
|
||||
import { QqbotFflogsPluginService } from '../plugins/fflogs/qqbot-fflogs.plugin';
|
||||
import type {
|
||||
@ -15,11 +16,13 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
||||
private readonly plugins = new Map<string, QqbotIntegrationPlugin>();
|
||||
|
||||
constructor(
|
||||
private readonly bangDreamPlugin: QqbotBangDreamPluginService,
|
||||
private readonly ff14MarketPlugin: QqbotFf14MarketPluginService,
|
||||
private readonly fflogsPlugin: QqbotFflogsPluginService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.register(this.bangDreamPlugin.getPlugin());
|
||||
this.register(this.ff14MarketPlugin.getPlugin());
|
||||
this.register(this.fflogsPlugin.getPlugin());
|
||||
}
|
||||
|
||||
274
src/qqbot/plugins/bangDream/qqbot-bangdream-client.service.ts
Normal file
274
src/qqbot/plugins/bangDream/qqbot-bangdream-client.service.ts
Normal file
@ -0,0 +1,274 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type {
|
||||
BestdoriBandListItem,
|
||||
BestdoriLocalizedText,
|
||||
BestdoriSongInfo,
|
||||
BestdoriSongListItem,
|
||||
QqbotBangDreamSongSearchInput,
|
||||
QqbotBangDreamSongSummary,
|
||||
} from './qqbot-bangdream.types';
|
||||
|
||||
type BangDreamSongIndexItem = {
|
||||
id: number;
|
||||
keys: string[];
|
||||
title: string;
|
||||
titles: string[];
|
||||
};
|
||||
|
||||
const BESTDORI_API_BASE_URL = 'https://bestdori.com/api';
|
||||
const BESTDORI_WEB_BASE_URL = 'https://bestdori.com';
|
||||
const CACHE_TTL_MS = 10 * 60 * 1000;
|
||||
const REQUEST_TIMEOUT_MS = 8_000;
|
||||
const TEXT_INDEX_PRIORITY = [3, 1, 2, 0, 4];
|
||||
const DIFFICULTY_LABELS: Record<string, string> = {
|
||||
'0': 'EASY',
|
||||
'1': 'NORMAL',
|
||||
'2': 'HARD',
|
||||
'3': 'EXPERT',
|
||||
'4': 'SPECIAL',
|
||||
};
|
||||
const TAG_LABELS: Record<string, string> = {
|
||||
normal: '原创',
|
||||
cover: '翻唱',
|
||||
tie_up: '联动',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class QqbotBangDreamClientService {
|
||||
private bandCache?: {
|
||||
expiresAt: number;
|
||||
map: Map<number, string>;
|
||||
};
|
||||
private songIndexCache?: {
|
||||
entries: BangDreamSongIndexItem[];
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
async checkHealth() {
|
||||
await this.getSongIndex();
|
||||
return true;
|
||||
}
|
||||
|
||||
async searchSong(
|
||||
params: QqbotBangDreamSongSearchInput,
|
||||
): Promise<QqbotBangDreamSongSummary> {
|
||||
const query = this.pickQuery(params);
|
||||
if (!query) throw new Error('请提供 BangDream 歌曲名或歌曲 ID');
|
||||
|
||||
const matched = await this.findSong(query);
|
||||
if (!matched) throw new Error(`未找到 BangDream 歌曲:${query}`);
|
||||
|
||||
const [song, bands] = await Promise.all([
|
||||
this.requestJson<BestdoriSongInfo>(`/songs/${matched.id}.json`),
|
||||
this.getBandMap(),
|
||||
]);
|
||||
return this.buildSongSummary(matched.id, song, bands);
|
||||
}
|
||||
|
||||
private async findSong(query: string) {
|
||||
const songId = Number(query);
|
||||
const entries = await this.getSongIndex();
|
||||
if (Number.isInteger(songId) && songId > 0) {
|
||||
return entries.find((item) => item.id === songId) || null;
|
||||
}
|
||||
|
||||
const key = this.normalizeLookupKey(query);
|
||||
const exact = entries.find((item) => item.keys.includes(key));
|
||||
if (exact) return exact;
|
||||
|
||||
return (
|
||||
entries.find((item) => item.keys.some((itemKey) => itemKey.includes(key))) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private async getSongIndex() {
|
||||
if (this.songIndexCache && Date.now() < this.songIndexCache.expiresAt) {
|
||||
return this.songIndexCache.entries;
|
||||
}
|
||||
|
||||
const data =
|
||||
await this.requestJson<Record<string, BestdoriSongListItem | null>>(
|
||||
'/songs/all.1.json',
|
||||
);
|
||||
const entries = Object.entries(data)
|
||||
.map(([id, item]) => this.toSongIndexItem(Number(id), item))
|
||||
.filter(Boolean) as BangDreamSongIndexItem[];
|
||||
this.songIndexCache = {
|
||||
entries,
|
||||
expiresAt: Date.now() + CACHE_TTL_MS,
|
||||
};
|
||||
return entries;
|
||||
}
|
||||
|
||||
private toSongIndexItem(id: number, item?: BestdoriSongListItem | null) {
|
||||
if (!Number.isInteger(id) || !item?.musicTitle) return null;
|
||||
const titles = this.pickAllTexts(item.musicTitle);
|
||||
if (!titles.length) return null;
|
||||
return {
|
||||
id,
|
||||
keys: [...new Set(titles.map((title) => this.normalizeLookupKey(title)))],
|
||||
title: this.pickLocalizedText(item.musicTitle),
|
||||
titles,
|
||||
} satisfies BangDreamSongIndexItem;
|
||||
}
|
||||
|
||||
private async getBandMap() {
|
||||
if (this.bandCache && Date.now() < this.bandCache.expiresAt) {
|
||||
return this.bandCache.map;
|
||||
}
|
||||
|
||||
const data =
|
||||
await this.requestJson<Record<string, BestdoriBandListItem | null>>(
|
||||
'/bands/all.1.json',
|
||||
);
|
||||
const map = new Map<number, string>();
|
||||
for (const [id, item] of Object.entries(data)) {
|
||||
const bandId = Number(id);
|
||||
const bandName = this.pickLocalizedText(item?.bandName);
|
||||
if (Number.isInteger(bandId) && bandName) map.set(bandId, bandName);
|
||||
}
|
||||
this.bandCache = {
|
||||
expiresAt: Date.now() + CACHE_TTL_MS,
|
||||
map,
|
||||
};
|
||||
return map;
|
||||
}
|
||||
|
||||
private buildSongSummary(
|
||||
id: number,
|
||||
song: BestdoriSongInfo,
|
||||
bands: Map<number, string>,
|
||||
): QqbotBangDreamSongSummary {
|
||||
const title = this.pickLocalizedText(song.musicTitle) || `歌曲 ${id}`;
|
||||
const bandName = bands.get(Number(song.bandId)) || '未知乐队';
|
||||
const result = {
|
||||
bandName,
|
||||
bpmText: this.formatBpm(song.bpm),
|
||||
difficultyText: this.formatDifficulty(song.difficulty),
|
||||
id,
|
||||
lengthText: this.formatLength(song.length),
|
||||
notesText: this.formatNotes(song.notes),
|
||||
publishedText: this.formatPublishedAt(song.publishedAt),
|
||||
tagText: TAG_LABELS[song.tag || ''] || song.tag || '未知类型',
|
||||
title,
|
||||
url: `${BESTDORI_WEB_BASE_URL}/info/songs/${id}`,
|
||||
} as QqbotBangDreamSongSummary;
|
||||
result.replyText = this.buildReplyText(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private buildReplyText(result: Omit<QqbotBangDreamSongSummary, 'replyText'>) {
|
||||
return [
|
||||
`BangDream 歌曲:${result.title}`,
|
||||
`ID:${result.id}|乐队:${result.bandName}|类型:${result.tagText}`,
|
||||
`时长:${result.lengthText}|BPM:${result.bpmText}`,
|
||||
`难度:${result.difficultyText}`,
|
||||
`物量:${result.notesText}`,
|
||||
result.publishedText ? `国服上线:${result.publishedText}` : '',
|
||||
result.url,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
private pickQuery(params: QqbotBangDreamSongSearchInput) {
|
||||
return `${params.query || params.text || params.raw || ''}`.trim();
|
||||
}
|
||||
|
||||
private pickLocalizedText(values?: BestdoriLocalizedText) {
|
||||
if (!Array.isArray(values)) return '';
|
||||
for (const index of TEXT_INDEX_PRIORITY) {
|
||||
const value = values[index];
|
||||
if (value) return value;
|
||||
}
|
||||
return values.find((value) => !!value) || '';
|
||||
}
|
||||
|
||||
private pickAllTexts(values: BestdoriLocalizedText) {
|
||||
return [
|
||||
...new Set(
|
||||
values.map((value) => `${value || ''}`.trim()).filter(Boolean),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private formatDifficulty(value?: BestdoriSongInfo['difficulty']) {
|
||||
const parts = Object.entries(value || {}).map(([key, item]) => {
|
||||
const label = DIFFICULTY_LABELS[key] || key;
|
||||
return `${label}${item?.playLevel ?? '-'}`;
|
||||
});
|
||||
return parts.length ? parts.join(' / ') : '暂无';
|
||||
}
|
||||
|
||||
private formatNotes(value?: BestdoriSongInfo['notes']) {
|
||||
const parts = Object.entries(value || {}).map(([key, notes]) => {
|
||||
const label = DIFFICULTY_LABELS[key] || key;
|
||||
return `${label}${notes ?? '-'}`;
|
||||
});
|
||||
return parts.length ? parts.join(' / ') : '暂无';
|
||||
}
|
||||
|
||||
private formatBpm(value?: BestdoriSongInfo['bpm']) {
|
||||
const bpms = Object.values(value || {})
|
||||
.flat()
|
||||
.map((item) => Number(item?.bpm))
|
||||
.filter((item) => Number.isFinite(item));
|
||||
if (!bpms.length) return '暂无';
|
||||
const unique = [...new Set(bpms)].sort((left, right) => left - right);
|
||||
if (unique.length === 1) return `${unique[0]}`;
|
||||
return `${unique[0]}-${unique[unique.length - 1]}`;
|
||||
}
|
||||
|
||||
private formatLength(value?: number) {
|
||||
const seconds = Number(value);
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) return '暂无';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const rest = Math.round(seconds % 60);
|
||||
return `${minutes}:${`${rest}`.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
private formatPublishedAt(values?: BestdoriLocalizedText) {
|
||||
const value = values?.[3];
|
||||
const time = Number(value);
|
||||
if (!Number.isFinite(time) || time <= 0) return '';
|
||||
return new Date(time).toLocaleDateString('zh-CN', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeLookupKey(value: string) {
|
||||
return `${value || ''}`
|
||||
.normalize('NFKC')
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '');
|
||||
}
|
||||
|
||||
private async requestJson<T>(path: string): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await fetch(`${BESTDORI_API_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'kt-template-online-api/qqbot',
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Bestdori 接口失败:${response.status}`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
} catch (err) {
|
||||
if ((err as Error)?.name === 'AbortError') {
|
||||
throw new Error('Bestdori 接口请求超时');
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/qqbot/plugins/bangDream/qqbot-bangdream.plugin.ts
Normal file
74
src/qqbot/plugins/bangDream/qqbot-bangdream.plugin.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { formatKtDateTime, ToolsService } from '@/common';
|
||||
import type { QqbotIntegrationPlugin } from '../../qqbot.types';
|
||||
import { QqbotBangDreamClientService } from './qqbot-bangdream-client.service';
|
||||
|
||||
@Injectable()
|
||||
export class QqbotBangDreamPluginService {
|
||||
constructor(
|
||||
private readonly bangDreamClientService: QqbotBangDreamClientService,
|
||||
private readonly toolsService: ToolsService,
|
||||
) {}
|
||||
|
||||
getPlugin(): QqbotIntegrationPlugin {
|
||||
return {
|
||||
description:
|
||||
'对接 Bestdori 公开数据,提供 BanG Dream! Girls Band Party 歌曲查询能力。',
|
||||
healthCheck: async () => {
|
||||
const checkedAt = formatKtDateTime(new Date());
|
||||
try {
|
||||
await this.bangDreamClientService.checkHealth();
|
||||
return {
|
||||
checkedAt,
|
||||
message: 'BangDream 插件可用',
|
||||
status: 'healthy',
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
checkedAt,
|
||||
message: this.toolsService.getErrorMessage(
|
||||
err,
|
||||
'BangDream 插件不可用',
|
||||
),
|
||||
status: 'degraded',
|
||||
};
|
||||
}
|
||||
},
|
||||
key: 'bangDream',
|
||||
name: 'BangDream 查询',
|
||||
operations: [
|
||||
{
|
||||
cacheTtlMs: 60_000,
|
||||
description: '按歌曲名或 Bestdori 歌曲 ID 查询 BanG Dream 歌曲信息。',
|
||||
inputSchema: {
|
||||
properties: {
|
||||
query: { description: '歌曲名或歌曲 ID', type: 'string' },
|
||||
text: { description: '命令原始文本', type: 'string' },
|
||||
},
|
||||
required: ['query'],
|
||||
type: 'object',
|
||||
},
|
||||
key: 'bangdream.song.search',
|
||||
name: '歌曲查询',
|
||||
outputSchema: {
|
||||
properties: {
|
||||
bandName: { type: 'string' },
|
||||
bpmText: { type: 'string' },
|
||||
difficultyText: { type: 'string' },
|
||||
id: { type: 'number' },
|
||||
lengthText: { type: 'string' },
|
||||
notesText: { type: 'string' },
|
||||
replyText: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
execute: async (input) =>
|
||||
await this.bangDreamClientService.searchSong(input),
|
||||
},
|
||||
],
|
||||
version: '1.0.0',
|
||||
};
|
||||
}
|
||||
}
|
||||
40
src/qqbot/plugins/bangDream/qqbot-bangdream.types.ts
Normal file
40
src/qqbot/plugins/bangDream/qqbot-bangdream.types.ts
Normal file
@ -0,0 +1,40 @@
|
||||
export type BestdoriLocalizedText = Array<null | string>;
|
||||
|
||||
export type BestdoriSongListItem = {
|
||||
musicTitle?: BestdoriLocalizedText;
|
||||
};
|
||||
|
||||
export type BestdoriSongInfo = {
|
||||
bandId?: number;
|
||||
bpm?: Record<string, Array<{ bpm?: number }>>;
|
||||
difficulty?: Record<string, { playLevel?: number }>;
|
||||
length?: number;
|
||||
musicTitle?: BestdoriLocalizedText;
|
||||
notes?: Record<string, number>;
|
||||
publishedAt?: BestdoriLocalizedText;
|
||||
tag?: string;
|
||||
};
|
||||
|
||||
export type BestdoriBandListItem = {
|
||||
bandName?: BestdoriLocalizedText;
|
||||
};
|
||||
|
||||
export type QqbotBangDreamSongSearchInput = {
|
||||
query?: string;
|
||||
raw?: string;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export type QqbotBangDreamSongSummary = {
|
||||
bandName: string;
|
||||
bpmText: string;
|
||||
difficultyText: string;
|
||||
id: number;
|
||||
lengthText: string;
|
||||
notesText: string;
|
||||
publishedText: string;
|
||||
replyText: string;
|
||||
tagText: string;
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
@ -38,6 +38,8 @@ import { QqbotPermissionService } from './permission/qqbot-permission.service';
|
||||
import { QqbotEventPluginRegistryService } from './plugin/qqbot-event-plugin-registry.service';
|
||||
import { QqbotPluginController } from './plugin/qqbot-plugin.controller';
|
||||
import { QqbotPluginRegistryService } from './plugin/qqbot-plugin-registry.service';
|
||||
import { QqbotBangDreamClientService } from './plugins/bangDream/qqbot-bangdream-client.service';
|
||||
import { QqbotBangDreamPluginService } from './plugins/bangDream/qqbot-bangdream.plugin';
|
||||
import { QqbotFf14ClientService } from './plugins/ff14Market/qqbot-ff14-client.service';
|
||||
import { QqbotFf14MarketPluginService } from './plugins/ff14Market/qqbot-ff14-market.plugin';
|
||||
import { QqbotFflogsClientService } from './plugins/fflogs/qqbot-fflogs-client.service';
|
||||
@ -94,6 +96,8 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
||||
QqbotDashboardService,
|
||||
QqbotDedupeService,
|
||||
QqbotEventService,
|
||||
QqbotBangDreamClientService,
|
||||
QqbotBangDreamPluginService,
|
||||
QqbotFf14ClientService,
|
||||
QqbotFf14MarketPluginService,
|
||||
QqbotFflogsClientService,
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
import { QqbotBangDreamClientService } from '@/qqbot/plugins/bangDream/qqbot-bangdream-client.service';
|
||||
|
||||
describe('QqbotBangDreamClientService', () => {
|
||||
let fetchSpy: jest.SpyInstance;
|
||||
let service: QqbotBangDreamClientService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new QqbotBangDreamClientService();
|
||||
fetchSpy = jest.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
|
||||
const target = `${url}`;
|
||||
if (target.endsWith('/songs/all.1.json')) {
|
||||
return jsonResponse({
|
||||
'180': {
|
||||
musicTitle: ['Returns', 'Returns', 'Returns', 'Returns', 'Returns'],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (target.endsWith('/songs/180.json')) {
|
||||
return jsonResponse({
|
||||
bandId: 1,
|
||||
bpm: {
|
||||
'3': [{ bpm: 185 }],
|
||||
},
|
||||
difficulty: {
|
||||
'0': { playLevel: 7 },
|
||||
'1': { playLevel: 14 },
|
||||
'2': { playLevel: 19 },
|
||||
'3': { playLevel: 25 },
|
||||
},
|
||||
length: 132.36,
|
||||
musicTitle: ['Returns', 'Returns', 'Returns', 'Returns', 'Returns'],
|
||||
notes: {
|
||||
'0': 140,
|
||||
'1': 269,
|
||||
'2': 473,
|
||||
'3': 698,
|
||||
},
|
||||
publishedAt: [
|
||||
'1553234400000',
|
||||
'1584864000000',
|
||||
'1563519600000',
|
||||
'1577854800000',
|
||||
'1553234400000',
|
||||
],
|
||||
tag: 'normal',
|
||||
});
|
||||
}
|
||||
if (target.endsWith('/bands/all.1.json')) {
|
||||
return jsonResponse({
|
||||
'1': {
|
||||
bandName: [
|
||||
"Poppin'Party",
|
||||
"Poppin'Party",
|
||||
"Poppin'Party",
|
||||
"Poppin'Party",
|
||||
"Poppin'Party",
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
return jsonResponse({}, 404);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('searches Bestdori song by title and builds localized QQ reply text', async () => {
|
||||
const result = await service.searchSong({ text: 'Returns' });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
bandName: "Poppin'Party",
|
||||
bpmText: '185',
|
||||
difficultyText: 'EASY7 / NORMAL14 / HARD19 / EXPERT25',
|
||||
id: 180,
|
||||
lengthText: '2:12',
|
||||
notesText: 'EASY140 / NORMAL269 / HARD473 / EXPERT698',
|
||||
tagText: '原创',
|
||||
title: 'Returns',
|
||||
url: 'https://bestdori.com/info/songs/180',
|
||||
});
|
||||
expect(result.replyText).toContain('BangDream 歌曲:Returns');
|
||||
expect(result.replyText).toContain('乐队:Poppin');
|
||||
expect(result.replyText).toContain('难度:EASY7 / NORMAL14');
|
||||
});
|
||||
|
||||
it('searches Bestdori song by numeric id', async () => {
|
||||
const result = await service.searchSong({ text: '180' });
|
||||
|
||||
expect(result.id).toBe(180);
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
'https://bestdori.com/api/songs/180.json',
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function jsonResponse(body: unknown, status = 200) {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify(body), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status,
|
||||
}),
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user