Files
QZMusic_PC-pre/amll-local/packages/lyric/test/lrc.test.ts
lqtmcstudio 72f4510dc8 fork(fix): Clone AMLL 并修复 BUG
- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork)
- 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
2026-06-07 00:02:14 +08:00

202 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import { parseLrc, stringifyLrc } from "../src/formats/lrc";
import { MAX_LRC_TIMESTAMP } from "../src/utils";
import { timeStampsTestCases } from "./timestampcase.fixture";
describe("lrc", () => {
it("parses basic timestamped lines", () => {
const lines = parseLrc("[00:01.120]Hello\n[00:03.000]World");
expect(lines).toHaveLength(2);
expect(lines[0].startTime).toBe(1120);
expect(lines[0].endTime).toBe(3000);
expect(lines[0].words[0].word).toBe("Hello");
expect(lines[1].startTime).toBe(3000);
expect(lines[1].words[0].word).toBe("World");
});
it("handles CRLF line breaks correctly", () => {
const lines = parseLrc("[00:01.120]Hello\r\n[00:03.000]World");
expect(lines).toHaveLength(2);
expect(lines[0].startTime).toBe(1120);
expect(lines[0].endTime).toBe(3000);
expect(lines[0].words[0].word).toBe("Hello");
expect(lines[1].startTime).toBe(3000);
expect(lines[1].words[0].word).toBe("World");
});
it("parses multiple timestamps for the same line", () => {
const lines = parseLrc("[00:01.120][00:02.000]Hello");
expect(lines).toHaveLength(2);
expect(lines[0].startTime).toBe(1120);
expect(lines[0].endTime).toBe(2000);
expect(lines[0].words[0].word).toBe("Hello");
expect(lines[1].startTime).toBe(2000);
expect(lines[1].words[0].word).toBe("Hello");
});
it("ignores lines without timestamps", () => {
const lines = parseLrc(
"This is a line without timestamp\n[ar: Artist]\n[00:01.120]Hello\n# This is a comment\n{ some: 'metadata' }\n\n[00:03.000]World",
);
expect(lines).toHaveLength(2);
expect(lines[0].startTime).toBe(1120);
expect(lines[0].endTime).toBe(3000);
expect(lines[0].words[0].word).toBe("Hello");
expect(lines[1].startTime).toBe(3000);
expect(lines[1].words[0].word).toBe("World");
});
it("ignores lines with bad timestamps", () => {
const lines = parseLrc(
"[00:01.120]Hello\n[invalid]Bad line\n[xx:yy.zzz]Bad line\n[-1:00.000]Bad line\n[NaN:NaN]Bad line\n[00:03.000]World",
);
expect(lines).toHaveLength(2);
expect(lines[0].startTime).toBe(1120);
expect(lines[0].endTime).toBe(3000);
expect(lines[0].words[0].word).toBe("Hello");
expect(lines[1].startTime).toBe(3000);
expect(lines[1].words[0].word).toBe("World");
});
it("sorts lines by timestamp and sets end times correctly", () => {
const lines = parseLrc("[00:03.000]World\n[00:01.120][00:05.000]Hello");
expect(lines).toHaveLength(3);
expect(lines[0].startTime).toBe(1120);
expect(lines[0].endTime).toBe(3000);
expect(lines[0].words[0].word).toBe("Hello");
expect(lines[1].startTime).toBe(3000);
expect(lines[1].endTime).toBe(5000);
expect(lines[1].words[0].word).toBe("World");
expect(lines[2].startTime).toBe(5000);
expect(lines[2].endTime).toBe(MAX_LRC_TIMESTAMP);
expect(lines[2].words[0].word).toBe("Hello");
});
it("parses all kinds of timestamps", () => {
const input = timeStampsTestCases
.map(([ts, ms]) => `[${ts}]Should be ${ts} = ${ms} ms`)
.join("\n");
const lines = parseLrc(input);
expect(lines).toHaveLength(timeStampsTestCases.length);
lines.forEach((line, i) => {
const [ts, ms] = timeStampsTestCases[i];
expect(line.words[0].word).toBe(`Should be ${ts} = ${ms} ms`);
expect(line.startTime).toBe(ms);
});
});
it("identifies background lines with parentheses", () => {
const lines = parseLrc(
"[00:01.120](Hello)\n[00:03.000]Hi\n[00:03.000]World",
);
expect(lines).toHaveLength(3);
expect(lines[0].isBG).toBe(true);
expect(lines[0].words).toHaveLength(1);
expect(lines[0].words[0].word).toBe("Hello");
expect(lines[1].isBG).toBe(true);
expect(lines[1].words).toHaveLength(1);
expect(lines[1].words[0].word).toBe("Hi");
expect(lines[2].isBG).toBe(false);
expect(lines[2].words).toHaveLength(1);
expect(lines[2].words[0].word).toBe("World");
});
it("trims whitespace from lines and ignore empty lines, while preserving end times", () => {
const lines = parseLrc(
"[00:00.000]\n[00:01.000] \n[00:01.120] Hello \n[00:02.333]\n[00:03.000] World \n[00:05.000] \n",
);
expect(lines).toHaveLength(2);
expect(lines[0].words[0].word).toBe("Hello");
expect(lines[0].endTime).toBe(2333);
expect(lines[1].words[0].word).toBe("World");
expect(lines[1].endTime).toBe(5000);
});
it("stringifies background lines with parentheses and normal lines without", () => {
const lines = [
{
startTime: 1120,
endTime: 3000,
words: [
{ startTime: 1120, endTime: 3000, word: "Hello", romanWord: "" },
],
translatedLyric: "",
romanLyric: "",
isBG: true,
isDuet: false,
},
{
startTime: 3000,
endTime: 3000,
words: [
{ startTime: 3000, endTime: 3000, word: "World", romanWord: "" },
],
translatedLyric: "",
romanLyric: "",
isBG: false,
isDuet: false,
},
];
const result = stringifyLrc(lines);
expect(result).toBe("[00:01.120](Hello)\n[00:03.000]World");
});
it("stringifies lines to expected lrc text", () => {
const result = stringifyLrc([
{
startTime: 1120,
endTime: 3000,
words: [
{ startTime: 1120, endTime: 3000, word: "Hello", romanWord: "" },
{ startTime: 0, endTime: 0, word: " ", romanWord: "" },
{ startTime: 1120, endTime: 3000, word: "world!", romanWord: "" },
],
translatedLyric: "",
romanLyric: "",
isBG: false,
isDuet: false,
},
]);
expect(result).toBe("[00:01.120]Hello world!");
});
it("normalizes invalid startTime when stringifying", () => {
const baseLine = {
endTime: 3000,
words: [{ startTime: 0, endTime: 0, word: "Hello", romanWord: "" }],
translatedLyric: "",
romanLyric: "",
isBG: false,
isDuet: false,
};
expect(stringifyLrc([{ ...baseLine, startTime: -1 }])).toBe(
"[00:00.000]Hello",
);
expect(stringifyLrc([{ ...baseLine, startTime: Number.NaN }])).toBe(
"[00:00.000]Hello",
);
expect(
stringifyLrc([{ ...baseLine, startTime: Number.POSITIVE_INFINITY }]),
).toBe("[00:00.000]Hello");
});
it("keeps parse -> stringify -> parse stable for content and timing", () => {
const input = "[00:01.120]Hello\n[00:03.000](World)";
const first = parseLrc(input);
const text = stringifyLrc(first);
const second = parseLrc(text);
expect(second).toEqual(first);
});
});