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`)
|
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
|
VALUES
|
||||||
(2041700000000300501, 'ff14_price', 'FF14 查价', '["查价","price","ff14price"]', '["/","!","!"]', 'ff14Market', 'ff14.market.price', 'ff14Price', 'all', '{"language":"chs","world":"中国"}', '', 'FF14 查价失败:{{error}}', 1, 0, 1500, '默认示例命令;请在账号配置中绑定后启用'),
|
(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
|
ON DUPLICATE KEY UPDATE
|
||||||
`name` = VALUES(`name`),
|
`name` = VALUES(`name`),
|
||||||
`plugin_key` = VALUES(`plugin_key`),
|
`plugin_key` = VALUES(`plugin_key`),
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import { formatKtDateTime, throwVbenError } from '@/common';
|
import { formatKtDateTime, throwVbenError } from '@/common';
|
||||||
|
import { QqbotBangDreamPluginService } from '../plugins/bangDream/qqbot-bangdream.plugin';
|
||||||
import { QqbotFf14MarketPluginService } from '../plugins/ff14Market/qqbot-ff14-market.plugin';
|
import { QqbotFf14MarketPluginService } from '../plugins/ff14Market/qqbot-ff14-market.plugin';
|
||||||
import { QqbotFflogsPluginService } from '../plugins/fflogs/qqbot-fflogs.plugin';
|
import { QqbotFflogsPluginService } from '../plugins/fflogs/qqbot-fflogs.plugin';
|
||||||
import type {
|
import type {
|
||||||
@ -15,11 +16,13 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
private readonly plugins = new Map<string, QqbotIntegrationPlugin>();
|
private readonly plugins = new Map<string, QqbotIntegrationPlugin>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly bangDreamPlugin: QqbotBangDreamPluginService,
|
||||||
private readonly ff14MarketPlugin: QqbotFf14MarketPluginService,
|
private readonly ff14MarketPlugin: QqbotFf14MarketPluginService,
|
||||||
private readonly fflogsPlugin: QqbotFflogsPluginService,
|
private readonly fflogsPlugin: QqbotFflogsPluginService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
|
this.register(this.bangDreamPlugin.getPlugin());
|
||||||
this.register(this.ff14MarketPlugin.getPlugin());
|
this.register(this.ff14MarketPlugin.getPlugin());
|
||||||
this.register(this.fflogsPlugin.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 { QqbotEventPluginRegistryService } from './plugin/qqbot-event-plugin-registry.service';
|
||||||
import { QqbotPluginController } from './plugin/qqbot-plugin.controller';
|
import { QqbotPluginController } from './plugin/qqbot-plugin.controller';
|
||||||
import { QqbotPluginRegistryService } from './plugin/qqbot-plugin-registry.service';
|
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 { QqbotFf14ClientService } from './plugins/ff14Market/qqbot-ff14-client.service';
|
||||||
import { QqbotFf14MarketPluginService } from './plugins/ff14Market/qqbot-ff14-market.plugin';
|
import { QqbotFf14MarketPluginService } from './plugins/ff14Market/qqbot-ff14-market.plugin';
|
||||||
import { QqbotFflogsClientService } from './plugins/fflogs/qqbot-fflogs-client.service';
|
import { QqbotFflogsClientService } from './plugins/fflogs/qqbot-fflogs-client.service';
|
||||||
@ -94,6 +96,8 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
|||||||
QqbotDashboardService,
|
QqbotDashboardService,
|
||||||
QqbotDedupeService,
|
QqbotDedupeService,
|
||||||
QqbotEventService,
|
QqbotEventService,
|
||||||
|
QqbotBangDreamClientService,
|
||||||
|
QqbotBangDreamPluginService,
|
||||||
QqbotFf14ClientService,
|
QqbotFf14ClientService,
|
||||||
QqbotFf14MarketPluginService,
|
QqbotFf14MarketPluginService,
|
||||||
QqbotFflogsClientService,
|
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