fix: 降低BangDream歌曲详情渲染峰值
This commit is contained in:
parent
0fe3f99c36
commit
1deb2dc8b6
@ -78,6 +78,7 @@ export class Song {
|
||||
|
||||
//meta数据
|
||||
hasMeta = false;
|
||||
private readonly songJacketImageCache = new Map<string, Promise<Image>>();
|
||||
|
||||
meta: {
|
||||
[difficultyId: number]: {
|
||||
@ -206,6 +207,18 @@ export class Song {
|
||||
*/
|
||||
async getSongJacketImage(
|
||||
displayedServerList: Server[] = [Server.jp, Server.cn],
|
||||
): Promise<Image> {
|
||||
const cacheKey = displayedServerList.join(',');
|
||||
let jacketImage = this.songJacketImageCache.get(cacheKey);
|
||||
if (!jacketImage) {
|
||||
jacketImage = this.loadSongJacketImage(displayedServerList);
|
||||
this.songJacketImageCache.set(cacheKey, jacketImage);
|
||||
}
|
||||
return await jacketImage;
|
||||
}
|
||||
|
||||
private async loadSongJacketImage(
|
||||
displayedServerList: Server[],
|
||||
): Promise<Image> {
|
||||
const jacketImageBuffer = await songResourceRepository.getJacketImageBuffer(
|
||||
this,
|
||||
@ -357,6 +370,16 @@ export interface SongInRank {
|
||||
meta: number;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
export interface SongMetaRankSummary {
|
||||
entries: Array<{
|
||||
difficulty: number;
|
||||
meta: number;
|
||||
rank: number;
|
||||
}>;
|
||||
maxMeta: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在BangDream 领域模型层中获取MetaRanking。
|
||||
*
|
||||
@ -404,3 +427,87 @@ export function getMetaRanking(
|
||||
}
|
||||
return songRankList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在BangDream 领域模型层中获取指定歌曲的Meta排名摘要。
|
||||
*
|
||||
* @param targetSong - 目标歌曲。
|
||||
* @param withFever - withFever参数。
|
||||
* @param mainServer - 主数据服务器参数。
|
||||
* @returns 目标歌曲的排名条目与全局最大Meta。
|
||||
*/
|
||||
export function getSongMetaRankSummary(
|
||||
targetSong: Song,
|
||||
withFever: boolean,
|
||||
mainServer: Server,
|
||||
): SongMetaRankSummary {
|
||||
const songIdList = bangdreamCatalogRepository.getNumericIds('meta');
|
||||
const rowMetas: number[] = [];
|
||||
const targetEntries: Array<{
|
||||
difficulty: number;
|
||||
meta: number;
|
||||
order: number;
|
||||
}> = [];
|
||||
let maxMeta = 0;
|
||||
|
||||
for (let i = 0; i < songIdList.length; i++) {
|
||||
const songId = songIdList[i];
|
||||
const song = songId === targetSong.songId ? targetSong : new Song(songId);
|
||||
if (!isSongMetaRankCandidate(song, mainServer)) {
|
||||
continue;
|
||||
}
|
||||
for (const j in song.difficulty) {
|
||||
const difficulty = parseInt(j);
|
||||
const meta = song.calcMeta(withFever, difficulty);
|
||||
const order = rowMetas.length;
|
||||
rowMetas.push(meta);
|
||||
if (meta > maxMeta) {
|
||||
maxMeta = meta;
|
||||
}
|
||||
if (song.songId === targetSong.songId) {
|
||||
targetEntries.push({
|
||||
difficulty,
|
||||
meta,
|
||||
order,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
targetEntries.sort((a, b) => {
|
||||
const metaDiff = b.meta - a.meta;
|
||||
return metaDiff === 0 ? a.order - b.order : metaDiff;
|
||||
});
|
||||
|
||||
return {
|
||||
entries: targetEntries.map((entry) => ({
|
||||
difficulty: entry.difficulty,
|
||||
meta: entry.meta,
|
||||
rank: countStableMetaRank(rowMetas, entry.meta, entry.order),
|
||||
})),
|
||||
maxMeta,
|
||||
};
|
||||
}
|
||||
|
||||
function isSongMetaRankCandidate(song: Song, mainServer: Server): boolean {
|
||||
return (
|
||||
song.publishedAt[mainServer] != null &&
|
||||
Object.keys(song.notes).length > 0 &&
|
||||
song.hasMeta
|
||||
);
|
||||
}
|
||||
|
||||
function countStableMetaRank(
|
||||
rowMetas: number[],
|
||||
targetMeta: number,
|
||||
targetOrder: number,
|
||||
) {
|
||||
let rank = 0;
|
||||
for (let i = 0; i < rowMetas.length; i++) {
|
||||
const meta = rowMetas[i];
|
||||
if (meta > targetMeta || (meta === targetMeta && i < targetOrder)) {
|
||||
rank++;
|
||||
}
|
||||
}
|
||||
return rank;
|
||||
}
|
||||
|
||||
@ -12,7 +12,8 @@ import {
|
||||
import {
|
||||
Song,
|
||||
getMetaRanking,
|
||||
SongInRank,
|
||||
getSongMetaRankSummary,
|
||||
type SongMetaRankSummary,
|
||||
} from '@/modules/qqbot/plugins/bangdream/src/domain/song/song.model';
|
||||
import { drawDottedLine } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-dotted-line';
|
||||
import {
|
||||
@ -240,19 +241,9 @@ export async function drawSongMetaListDataBlock(
|
||||
topLeftText?: string,
|
||||
displayedServerList: Server[] = globalDefaultServer,
|
||||
) {
|
||||
const metaRanking = {};
|
||||
const metaRanking: Partial<Record<Server, SongMetaRankSummary>> = {};
|
||||
for (const server of displayedServerList) {
|
||||
metaRanking[server] = {};
|
||||
metaRanking[server].data = getMetaRanking(withFever, server);
|
||||
metaRanking[server].maxMeta = metaRanking[server].data[0].meta;
|
||||
}
|
||||
const songMetaRanking = {};
|
||||
for (const server of displayedServerList) {
|
||||
songMetaRanking[server] = {};
|
||||
const tempMetaRanking = metaRanking[server].data;
|
||||
songMetaRanking[server].data = tempMetaRanking.filter(
|
||||
(value: SongInRank) => value.songId == song.songId,
|
||||
);
|
||||
metaRanking[server] = getSongMetaRankSummary(song, withFever, server);
|
||||
}
|
||||
|
||||
const list: Array<Image | Canvas> = [];
|
||||
@ -260,14 +251,17 @@ export async function drawSongMetaListDataBlock(
|
||||
const difficultyId = parseInt(difficulty);
|
||||
let text = '';
|
||||
for (const server of displayedServerList) {
|
||||
const tempSongMetaRanking = songMetaRanking[server].data;
|
||||
for (let j = 0; j < tempSongMetaRanking.length; j++) {
|
||||
if (tempSongMetaRanking[j].difficulty == difficultyId) {
|
||||
const summary = metaRanking[server];
|
||||
if (!summary) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < summary.entries.length; j++) {
|
||||
if (summary.entries[j].difficulty == difficultyId) {
|
||||
const percent = getRelativeMetaPercent(
|
||||
tempSongMetaRanking[j].meta,
|
||||
metaRanking[server].maxMeta,
|
||||
summary.entries[j].meta,
|
||||
summary.maxMeta,
|
||||
);
|
||||
text += `${serverNameFullList[server]}: ${percent}% #${tempSongMetaRanking[j].rank + 1} `;
|
||||
text += `${serverNameFullList[server]}: ${percent}% #${summary.entries[j].rank + 1} `;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
150
test/qqbot/plugins/bangdream/song/song-meta-summary.spec.ts
Normal file
150
test/qqbot/plugins/bangdream/song/song-meta-summary.spec.ts
Normal file
@ -0,0 +1,150 @@
|
||||
describe('BangDream song meta rank summary', () => {
|
||||
it('matches full meta ranking entries for a single song', async () => {
|
||||
jest.resetModules();
|
||||
jest.doMock(
|
||||
'@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: createCatalogFixture(),
|
||||
}),
|
||||
);
|
||||
|
||||
const { Server } = await import(
|
||||
'@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model'
|
||||
);
|
||||
const songModule = await import(
|
||||
'@/modules/qqbot/plugins/bangdream/src/domain/song/song.model'
|
||||
);
|
||||
const { Song, getMetaRanking } = songModule;
|
||||
const getSongMetaRankSummary = (
|
||||
songModule as typeof songModule & {
|
||||
getSongMetaRankSummary?: (
|
||||
song: InstanceType<typeof Song>,
|
||||
withFever: boolean,
|
||||
server: typeof Server.cn,
|
||||
) => {
|
||||
entries: Array<{ difficulty: number; meta: number; rank: number }>;
|
||||
maxMeta: number;
|
||||
};
|
||||
}
|
||||
).getSongMetaRankSummary;
|
||||
|
||||
const song = new Song(243);
|
||||
const fullRanking = getMetaRanking(true, Server.cn);
|
||||
expect(typeof getSongMetaRankSummary).toBe('function');
|
||||
const summary = getSongMetaRankSummary(song, true, Server.cn);
|
||||
|
||||
expect(summary.maxMeta).toBe(fullRanking[0].meta);
|
||||
expect(summary.entries).toEqual(
|
||||
fullRanking
|
||||
.filter((entry) => entry.songId === 243)
|
||||
.map(({ difficulty, meta, rank }) => ({ difficulty, meta, rank })),
|
||||
);
|
||||
});
|
||||
|
||||
it('caches jacket image decoding within a song instance', async () => {
|
||||
const getJacketImageBuffer = jest
|
||||
.fn<Promise<Buffer>, [unknown]>()
|
||||
.mockResolvedValue(Buffer.from('fake-png'));
|
||||
const loadImage = jest.fn(async () => ({
|
||||
height: 1,
|
||||
width: 1,
|
||||
}));
|
||||
|
||||
jest.resetModules();
|
||||
jest.doMock(
|
||||
'@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: createCatalogFixture(),
|
||||
}),
|
||||
);
|
||||
jest.doMock(
|
||||
'@/modules/qqbot/plugins/bangdream/src/domain/song/song-resource.repository',
|
||||
() => ({
|
||||
songResourceRepository: {
|
||||
getChart: jest.fn(),
|
||||
getDetail: jest.fn(),
|
||||
getJacketImageBuffer,
|
||||
getJacketImagePath: jest.fn(),
|
||||
getSongRip: jest.fn(),
|
||||
resolveJacketImageUrl: jest.fn(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
jest.doMock('skia-canvas', () => ({
|
||||
loadImage,
|
||||
}));
|
||||
|
||||
const { Song } = await import(
|
||||
'@/modules/qqbot/plugins/bangdream/src/domain/song/song.model'
|
||||
);
|
||||
|
||||
const song = new Song(243);
|
||||
const first = await song.getSongJacketImage();
|
||||
const second = await song.getSongJacketImage();
|
||||
|
||||
expect(first).toBe(second);
|
||||
expect(getJacketImageBuffer).toHaveBeenCalledTimes(1);
|
||||
expect(loadImage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
function createCatalogFixture() {
|
||||
return {
|
||||
meta: {
|
||||
100: createMeta({ 1: 90, 2: 60 }),
|
||||
243: createMeta({ 1: 50, 2: 70 }),
|
||||
300: createMeta({ 1: 100, 2: 100 }),
|
||||
400: createMeta({ 1: 70, 2: 70 }),
|
||||
},
|
||||
songs: {
|
||||
100: createSong({ id: 100, publishedCn: true }),
|
||||
243: createSong({ id: 243, publishedCn: true }),
|
||||
300: createSong({ id: 300, publishedCn: false }),
|
||||
400: createSong({ id: 400, publishedCn: true }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSong({
|
||||
id,
|
||||
publishedCn,
|
||||
}: {
|
||||
id: number;
|
||||
publishedCn: boolean;
|
||||
}) {
|
||||
return {
|
||||
bandId: 1,
|
||||
bpm: {
|
||||
1: [{ bpm: 120, end: 1, start: 0 }],
|
||||
2: [{ bpm: 140, end: 1, start: 0 }],
|
||||
},
|
||||
closedAt: [],
|
||||
difficulty: {
|
||||
1: { playLevel: 20 },
|
||||
2: { playLevel: 25 },
|
||||
},
|
||||
jacketImage: [`song-${id}`],
|
||||
length: 120,
|
||||
musicTitle: [`Song ${id}`, null, null, `歌曲 ${id}`, null],
|
||||
nickname: null,
|
||||
notes: {
|
||||
1: 500,
|
||||
2: 700,
|
||||
},
|
||||
publishedAt: [null, null, null, publishedCn ? 1 : null, null],
|
||||
tag: 'normal',
|
||||
};
|
||||
}
|
||||
|
||||
function createMeta(values: Record<number, number>) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(values).map(([difficulty, value]) => [
|
||||
difficulty,
|
||||
{
|
||||
7: [0, value, 0, value],
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user