LyricUtil.ets 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import fs from '@ohos.file.fs';
  2. import common from '@ohos.app.ability.common';
  3. import fileIo from '@ohos.file.fs';
  4. import { LyricWord } from '@seagazer/cclyric/src/main/ets/bean/LyricWord';
  5. // 定义接口来描述返回值的类型
  6. interface ParseResult {
  7. timeline: number;
  8. words: LyricWord[];
  9. }
  10. // 定义Navidrome歌词行的接口
  11. interface NavidromeLyricLine {
  12. start: number;
  13. value: string;
  14. }
  15. // 定义Navidrome语言数据的接口
  16. interface NavidromeLangData {
  17. lang?: string;
  18. line: NavidromeLyricLine[];
  19. }
  20. class LyricUtil {
  21. // 检测方括号格式的逐字歌词 [00:00.000]文[00:01.000]字
  22. private isSquareBracketWordByWordLyric(line: string): boolean {
  23. return /\[\d{2}:\d{2}\.\d{2,3}\]\S/.test(line);
  24. }
  25. // 解析方括号格式的逐字歌词行
  26. private parseSquareBracketWordLine(line: string, offset: number): ParseResult {
  27. const words: LyricWord[] = [];
  28. let firstTimeline = -1;
  29. // 正则匹配:[00:00.000]中文字
  30. const regex = /\[(\d{2}:\d{2}\.\d{2,3})\]([^\[]*)/g;
  31. // 显式指定 match 的类型
  32. let match: RegExpExecArray | null;
  33. while ((match = regex.exec(line))!== null) {
  34. const timeStr = match[1]; // 时间部分 00:00.000
  35. const word = match[2].trim(); // 歌词文本
  36. if (!word) continue; // 跳过空词
  37. const timeline = this.parseTimeline2(timeStr) - offset;
  38. if (firstTimeline < 0) firstTimeline = timeline;
  39. words.push(new LyricWord(word, timeline, 0));
  40. }
  41. // 计算每个词的持续时间
  42. for (let i = 0; i < words.length - 1; i++) {
  43. words[i].duration = words[i + 1].startTime - words[i].startTime;
  44. }
  45. if (words.length > 0 && words[words.length - 1].duration === 0) {
  46. words[words.length - 1].duration = 200; // 默认200ms
  47. }
  48. return { timeline: firstTimeline, words };
  49. }
  50. /******************** 时间解析增强 ********************/
  51. private parseTimeline2(timeString: string): number {
  52. // 增强支持毫秒/厘秒解析
  53. const parts = timeString.split(':');
  54. const minutes = parseInt(parts[0], 10);
  55. const secondParts = parts[1].split('.');
  56. const seconds = parseInt(secondParts[0], 10);
  57. const fraction = parseInt(secondParts[1], 10);
  58. // 根据小数位长度判断时间精度
  59. const milliseconds = secondParts[1].length === 2 ?
  60. fraction * 10 : // 厘秒转毫秒 (01 -> 10ms)
  61. fraction; // 毫秒直接使用
  62. return minutes * 60000 + seconds * 1000 + milliseconds;
  63. }
  64. // 判断是否是逐字歌词行
  65. private isWordByWordLyric(line: string): boolean {
  66. const hasBracketTimestamp = /\(\d{2}:\d{2}\.\d{2,3}\)\S/.test(line);
  67. const hasAngleTimestamp = /<\d{2}:\d{2}\.\d{2,3}>/.test(line);
  68. return hasBracketTimestamp || hasAngleTimestamp;
  69. }
  70. /**
  71. * 将整个逐字歌词内容转换为最简单的LRC格式
  72. * @param lyricContent 整个歌词内容(字符串)
  73. * @returns 转换后的LRC格式字符串
  74. */
  75. public convertLyricToSimpleLrc(lyricContent: string): string {
  76. let lyrics = lyricContent.split('\n').map(line => line.trim());
  77. const result: string[] = [];
  78. const ignoredTags = ['ti', 'ar', 'al', 'by', 'offset', 'hash', 'sign', 'qq', 'total', 'Outro', 'tool'];
  79. for (let i = 0; i < lyrics.length; i++) {
  80. const line = lyrics[i].trim();
  81. // 跳过空行和特定标签行
  82. if (!line) continue;
  83. if (ignoredTags.some(tag => line.startsWith(`[${tag}]`))) continue;
  84. // 处理逐字歌词行
  85. if (this.isSquareBracketWordByWordLyric(line) || this.isWordByWordLyric(line)) {
  86. let firstTimestamp = "";
  87. let fullText = "";
  88. // 处理尖括号格式:<00:00.000>文<00:01.000>字
  89. if (this.isWordByWordLyric(line)) {
  90. const regex = /<(\d{2}:\d{2}\.\d{2,3})>([^<]*)/g;
  91. let match: RegExpExecArray | null;
  92. while ((match = regex.exec(line)) !== null) {
  93. const timestamp = match[1];
  94. const word = match[2].trim();
  95. if (!firstTimestamp) firstTimestamp = timestamp;
  96. fullText += word;
  97. }
  98. }else
  99. // 处理方括号格式:[00:00.000]文[00:01.000]字
  100. if (this.isSquareBracketWordByWordLyric(line)) {
  101. const regex = /\[(\d{2}:\d{2}\.\d{2,3})\]([^\[]*)/g;
  102. let match: RegExpExecArray | null;
  103. while ((match = regex.exec(line)) !== null) {
  104. const timestamp = match[1];
  105. const word = match[2].trim();
  106. if (!firstTimestamp) firstTimestamp = timestamp;
  107. fullText += word;
  108. }
  109. }
  110. // 添加到结果中
  111. if (firstTimestamp && fullText) {
  112. result.push(`[${firstTimestamp}]${fullText}`);
  113. }
  114. }
  115. // 保留普通LRC行
  116. else {
  117. result.push(line);
  118. }
  119. }
  120. // 将结果数组连接成单一字符串
  121. return result.join('\n');
  122. }
  123. /**
  124. * 将Navidrome的JSON格式歌词转换为LRC标准格式
  125. * JSON格式: [{"lang":"xxx","line":[{"start":0,"value":"歌词内容"},...]}]
  126. * LRC格式: [00:00.00]歌词内容
  127. * @param jsonLyric Navidrome返回的JSON格式歌词
  128. * @returns 转换后的LRC格式歌词,如果转换失败返回undefined
  129. */
  130. public convertNavidromeJsonLyricToLrc(jsonLyric: string): string | undefined {
  131. if (!jsonLyric || jsonLyric.trim().length === 0) {
  132. return undefined;
  133. }
  134. try {
  135. // 尝试解析JSON
  136. const trimmed = jsonLyric.trim();
  137. // 检查是否是JSON格式(以[开头)
  138. if (!trimmed.startsWith('[')) {
  139. console.log("onecold LyricUtil: 歌词不是JSON格式,直接返回原歌词");
  140. return undefined;
  141. }
  142. console.log("onecold LyricUtil: 开始解析Navidrome JSON歌词");
  143. const jsonData = JSON.parse(trimmed) as NavidromeLangData[];
  144. if (!jsonData || jsonData.length === 0) {
  145. console.log("onecold LyricUtil: JSON歌词解析失败:数据为空");
  146. return undefined;
  147. }
  148. // 提取所有语言的歌词行,合并去重
  149. const allLyricLines: Map<number, string> = new Map();
  150. for (let i = 0; i < jsonData.length; i++) {
  151. const langData = jsonData[i];
  152. if (!langData.line || langData.line.length === 0) {
  153. continue;
  154. }
  155. // 遍历该语言的所有歌词行
  156. for (let j = 0; j < langData.line.length; j++) {
  157. const line = langData.line[j];
  158. if (line.start !== undefined && line.value) {
  159. // 如果该时间点还没有歌词,或者当前语言的歌词不为空,则添加
  160. const existing = allLyricLines.get(line.start);
  161. if (!existing || line.value.trim().length > 0) {
  162. allLyricLines.set(line.start, line.value);
  163. }
  164. }
  165. }
  166. }
  167. if (allLyricLines.size === 0) {
  168. console.log("onecold LyricUtil: 没有有效的歌词行");
  169. return undefined;
  170. }
  171. // 按时间戳排序
  172. const sortedTimes = Array.from(allLyricLines.keys()).sort((a, b) => a - b);
  173. // 转换为LRC格式
  174. const lrcLines: string[] = [];
  175. for (let k = 0; k < sortedTimes.length; k++) {
  176. const timeMs = sortedTimes[k];
  177. const text = allLyricLines.get(timeMs) ?? '';
  178. // 转换时间戳: 毫秒 -> [mm:ss.xx]
  179. const minutes = Math.floor(timeMs / 60000);
  180. const seconds = Math.floor((timeMs % 60000) / 1000);
  181. const centiseconds = Math.floor((timeMs % 1000) / 10);
  182. const timeTag = `[${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}]`;
  183. lrcLines.push(`${timeTag}${text}`);
  184. }
  185. const result = lrcLines.join('\n');
  186. console.log("onecold LyricUtil: Navidrome歌词转换成功,共" + lrcLines.length + "行");
  187. console.log("onecold LyricUtil: 转换后歌词预览:" + result.substring(0, 200));
  188. return result;
  189. } catch (error) {
  190. const err = error as Error;
  191. console.log("onecold LyricUtil: Navidrome歌词转换失败: " + err.message);
  192. return undefined;
  193. }
  194. }
  195. }
  196. export default new LyricUtil();