| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- import fs from '@ohos.file.fs';
- import common from '@ohos.app.ability.common';
- import fileIo from '@ohos.file.fs';
- import { LyricWord } from '@seagazer/cclyric/src/main/ets/bean/LyricWord';
- // 定义接口来描述返回值的类型
- interface ParseResult {
- timeline: number;
- words: LyricWord[];
- }
- // 定义Navidrome歌词行的接口
- interface NavidromeLyricLine {
- start: number;
- value: string;
- }
- // 定义Navidrome语言数据的接口
- interface NavidromeLangData {
- lang?: string;
- line: NavidromeLyricLine[];
- }
- class LyricUtil {
- // 检测方括号格式的逐字歌词 [00:00.000]文[00:01.000]字
- private isSquareBracketWordByWordLyric(line: string): boolean {
- return /\[\d{2}:\d{2}\.\d{2,3}\]\S/.test(line);
- }
- // 解析方括号格式的逐字歌词行
- private parseSquareBracketWordLine(line: string, offset: number): ParseResult {
- const words: LyricWord[] = [];
- let firstTimeline = -1;
- // 正则匹配:[00:00.000]中文字
- const regex = /\[(\d{2}:\d{2}\.\d{2,3})\]([^\[]*)/g;
- // 显式指定 match 的类型
- let match: RegExpExecArray | null;
- while ((match = regex.exec(line))!== null) {
- const timeStr = match[1]; // 时间部分 00:00.000
- const word = match[2].trim(); // 歌词文本
- if (!word) continue; // 跳过空词
- const timeline = this.parseTimeline2(timeStr) - offset;
- if (firstTimeline < 0) firstTimeline = timeline;
- words.push(new LyricWord(word, timeline, 0));
- }
- // 计算每个词的持续时间
- for (let i = 0; i < words.length - 1; i++) {
- words[i].duration = words[i + 1].startTime - words[i].startTime;
- }
- if (words.length > 0 && words[words.length - 1].duration === 0) {
- words[words.length - 1].duration = 200; // 默认200ms
- }
- return { timeline: firstTimeline, words };
- }
- /******************** 时间解析增强 ********************/
- private parseTimeline2(timeString: string): number {
- // 增强支持毫秒/厘秒解析
- const parts = timeString.split(':');
- const minutes = parseInt(parts[0], 10);
- const secondParts = parts[1].split('.');
- const seconds = parseInt(secondParts[0], 10);
- const fraction = parseInt(secondParts[1], 10);
- // 根据小数位长度判断时间精度
- const milliseconds = secondParts[1].length === 2 ?
- fraction * 10 : // 厘秒转毫秒 (01 -> 10ms)
- fraction; // 毫秒直接使用
- return minutes * 60000 + seconds * 1000 + milliseconds;
- }
- // 判断是否是逐字歌词行
- private isWordByWordLyric(line: string): boolean {
- const hasBracketTimestamp = /\(\d{2}:\d{2}\.\d{2,3}\)\S/.test(line);
- const hasAngleTimestamp = /<\d{2}:\d{2}\.\d{2,3}>/.test(line);
- return hasBracketTimestamp || hasAngleTimestamp;
- }
- /**
- * 将整个逐字歌词内容转换为最简单的LRC格式
- * @param lyricContent 整个歌词内容(字符串)
- * @returns 转换后的LRC格式字符串
- */
- public convertLyricToSimpleLrc(lyricContent: string): string {
- let lyrics = lyricContent.split('\n').map(line => line.trim());
- const result: string[] = [];
- const ignoredTags = ['ti', 'ar', 'al', 'by', 'offset', 'hash', 'sign', 'qq', 'total', 'Outro', 'tool'];
- for (let i = 0; i < lyrics.length; i++) {
- const line = lyrics[i].trim();
- // 跳过空行和特定标签行
- if (!line) continue;
- if (ignoredTags.some(tag => line.startsWith(`[${tag}]`))) continue;
- // 处理逐字歌词行
- if (this.isSquareBracketWordByWordLyric(line) || this.isWordByWordLyric(line)) {
- let firstTimestamp = "";
- let fullText = "";
- // 处理尖括号格式:<00:00.000>文<00:01.000>字
- if (this.isWordByWordLyric(line)) {
- const regex = /<(\d{2}:\d{2}\.\d{2,3})>([^<]*)/g;
- let match: RegExpExecArray | null;
- while ((match = regex.exec(line)) !== null) {
- const timestamp = match[1];
- const word = match[2].trim();
- if (!firstTimestamp) firstTimestamp = timestamp;
- fullText += word;
- }
- }else
- // 处理方括号格式:[00:00.000]文[00:01.000]字
- if (this.isSquareBracketWordByWordLyric(line)) {
- const regex = /\[(\d{2}:\d{2}\.\d{2,3})\]([^\[]*)/g;
- let match: RegExpExecArray | null;
- while ((match = regex.exec(line)) !== null) {
- const timestamp = match[1];
- const word = match[2].trim();
- if (!firstTimestamp) firstTimestamp = timestamp;
- fullText += word;
- }
- }
- // 添加到结果中
- if (firstTimestamp && fullText) {
- result.push(`[${firstTimestamp}]${fullText}`);
- }
- }
- // 保留普通LRC行
- else {
- result.push(line);
- }
- }
- // 将结果数组连接成单一字符串
- return result.join('\n');
- }
- /**
- * 将Navidrome的JSON格式歌词转换为LRC标准格式
- * JSON格式: [{"lang":"xxx","line":[{"start":0,"value":"歌词内容"},...]}]
- * LRC格式: [00:00.00]歌词内容
- * @param jsonLyric Navidrome返回的JSON格式歌词
- * @returns 转换后的LRC格式歌词,如果转换失败返回undefined
- */
- public convertNavidromeJsonLyricToLrc(jsonLyric: string): string | undefined {
- if (!jsonLyric || jsonLyric.trim().length === 0) {
- return undefined;
- }
- try {
- // 尝试解析JSON
- const trimmed = jsonLyric.trim();
- // 检查是否是JSON格式(以[开头)
- if (!trimmed.startsWith('[')) {
- console.log("onecold LyricUtil: 歌词不是JSON格式,直接返回原歌词");
- return undefined;
- }
- console.log("onecold LyricUtil: 开始解析Navidrome JSON歌词");
- const jsonData = JSON.parse(trimmed) as NavidromeLangData[];
- if (!jsonData || jsonData.length === 0) {
- console.log("onecold LyricUtil: JSON歌词解析失败:数据为空");
- return undefined;
- }
- // 提取所有语言的歌词行,合并去重
- const allLyricLines: Map<number, string> = new Map();
- for (let i = 0; i < jsonData.length; i++) {
- const langData = jsonData[i];
- if (!langData.line || langData.line.length === 0) {
- continue;
- }
- // 遍历该语言的所有歌词行
- for (let j = 0; j < langData.line.length; j++) {
- const line = langData.line[j];
- if (line.start !== undefined && line.value) {
- // 如果该时间点还没有歌词,或者当前语言的歌词不为空,则添加
- const existing = allLyricLines.get(line.start);
- if (!existing || line.value.trim().length > 0) {
- allLyricLines.set(line.start, line.value);
- }
- }
- }
- }
- if (allLyricLines.size === 0) {
- console.log("onecold LyricUtil: 没有有效的歌词行");
- return undefined;
- }
- // 按时间戳排序
- const sortedTimes = Array.from(allLyricLines.keys()).sort((a, b) => a - b);
- // 转换为LRC格式
- const lrcLines: string[] = [];
- for (let k = 0; k < sortedTimes.length; k++) {
- const timeMs = sortedTimes[k];
- const text = allLyricLines.get(timeMs) ?? '';
- // 转换时间戳: 毫秒 -> [mm:ss.xx]
- const minutes = Math.floor(timeMs / 60000);
- const seconds = Math.floor((timeMs % 60000) / 1000);
- const centiseconds = Math.floor((timeMs % 1000) / 10);
- const timeTag = `[${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}]`;
- lrcLines.push(`${timeTag}${text}`);
- }
- const result = lrcLines.join('\n');
- console.log("onecold LyricUtil: Navidrome歌词转换成功,共" + lrcLines.length + "行");
- console.log("onecold LyricUtil: 转换后歌词预览:" + result.substring(0, 200));
- return result;
- } catch (error) {
- const err = error as Error;
- console.log("onecold LyricUtil: Navidrome歌词转换失败: " + err.message);
- return undefined;
- }
- }
- }
- export default new LyricUtil();
|