fix: 降低BangDream歌曲详情渲染峰值
This commit is contained in:
parent
0fe3f99c36
commit
1deb2dc8b6
@ -78,6 +78,7 @@ export class Song {
|
|||||||
|
|
||||||
//meta数据
|
//meta数据
|
||||||
hasMeta = false;
|
hasMeta = false;
|
||||||
|
private readonly songJacketImageCache = new Map<string, Promise<Image>>();
|
||||||
|
|
||||||
meta: {
|
meta: {
|
||||||
[difficultyId: number]: {
|
[difficultyId: number]: {
|
||||||
@ -206,6 +207,18 @@ export class Song {
|
|||||||
*/
|
*/
|
||||||
async getSongJacketImage(
|
async getSongJacketImage(
|
||||||
displayedServerList: Server[] = [Server.jp, Server.cn],
|
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> {
|
): Promise<Image> {
|
||||||
const jacketImageBuffer = await songResourceRepository.getJacketImageBuffer(
|
const jacketImageBuffer = await songResourceRepository.getJacketImageBuffer(
|
||||||
this,
|
this,
|
||||||
@ -357,6 +370,16 @@ export interface SongInRank {
|
|||||||
meta: number;
|
meta: number;
|
||||||
rank: number;
|
rank: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SongMetaRankSummary {
|
||||||
|
entries: Array<{
|
||||||
|
difficulty: number;
|
||||||
|
meta: number;
|
||||||
|
rank: number;
|
||||||
|
}>;
|
||||||
|
maxMeta: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在BangDream 领域模型层中获取MetaRanking。
|
* 在BangDream 领域模型层中获取MetaRanking。
|
||||||
*
|
*
|
||||||
@ -404,3 +427,87 @@ export function getMetaRanking(
|
|||||||
}
|
}
|
||||||
return songRankList;
|
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 {
|
import {
|
||||||
Song,
|
Song,
|
||||||
getMetaRanking,
|
getMetaRanking,
|
||||||
SongInRank,
|
getSongMetaRankSummary,
|
||||||
|
type SongMetaRankSummary,
|
||||||
} from '@/modules/qqbot/plugins/bangdream/src/domain/song/song.model';
|
} from '@/modules/qqbot/plugins/bangdream/src/domain/song/song.model';
|
||||||
import { drawDottedLine } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-dotted-line';
|
import { drawDottedLine } from '@/modules/qqbot/plugins/bangdream/src/theme/canvas-dotted-line';
|
||||||
import {
|
import {
|
||||||
@ -240,19 +241,9 @@ export async function drawSongMetaListDataBlock(
|
|||||||
topLeftText?: string,
|
topLeftText?: string,
|
||||||
displayedServerList: Server[] = globalDefaultServer,
|
displayedServerList: Server[] = globalDefaultServer,
|
||||||
) {
|
) {
|
||||||
const metaRanking = {};
|
const metaRanking: Partial<Record<Server, SongMetaRankSummary>> = {};
|
||||||
for (const server of displayedServerList) {
|
for (const server of displayedServerList) {
|
||||||
metaRanking[server] = {};
|
metaRanking[server] = getSongMetaRankSummary(song, withFever, 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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const list: Array<Image | Canvas> = [];
|
const list: Array<Image | Canvas> = [];
|
||||||
@ -260,14 +251,17 @@ export async function drawSongMetaListDataBlock(
|
|||||||
const difficultyId = parseInt(difficulty);
|
const difficultyId = parseInt(difficulty);
|
||||||
let text = '';
|
let text = '';
|
||||||
for (const server of displayedServerList) {
|
for (const server of displayedServerList) {
|
||||||
const tempSongMetaRanking = songMetaRanking[server].data;
|
const summary = metaRanking[server];
|
||||||
for (let j = 0; j < tempSongMetaRanking.length; j++) {
|
if (!summary) {
|
||||||
if (tempSongMetaRanking[j].difficulty == difficultyId) {
|
continue;
|
||||||
|
}
|
||||||
|
for (let j = 0; j < summary.entries.length; j++) {
|
||||||
|
if (summary.entries[j].difficulty == difficultyId) {
|
||||||
const percent = getRelativeMetaPercent(
|
const percent = getRelativeMetaPercent(
|
||||||
tempSongMetaRanking[j].meta,
|
summary.entries[j].meta,
|
||||||
metaRanking[server].maxMeta,
|
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