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 = 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();