Skip to content

Commit 5d07916

Browse files
authored
Merge pull request #771 from imsyy/dev-fixerr
✨ feat: 增加更详细的日志输出
2 parents 726f10a + d847cf5 commit 5d07916

7 files changed

Lines changed: 277 additions & 156 deletions

File tree

src/components/Player/PlayerMeta/PlayerData.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,10 @@ const qualityOptions = computed<DropdownOption[]>(() => {
212212
const lyricMode = computed(() => {
213213
if (settingStore.showYrc) {
214214
if (statusStore.usingTTMLLyric) return "TTML";
215-
if (musicStore.isHasYrc) return "YRC";
215+
if (musicStore.isHasYrc) {
216+
// 如果是从QQ音乐获取的歌词,显示QRC
217+
return statusStore.usingQRCLyric ? "QRC" : "YRC";
218+
}
216219
}
217220
return musicStore.isHasLrc ? "LRC" : "NO-LRC";
218221
});

src/core/player/LyricManager.ts

Lines changed: 8 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useMusicStore, useSettingStore, useStatusStore, useStreamingStore } fro
66
import { type SongLyric } from "@/types/lyric";
77
import { SongType } from "@/types/main";
88
import { isElectron } from "@/utils/env";
9-
import { isWordLevelFormat, parseSmartLrc } from "@/utils/lyricParser";
9+
import { alignLyrics, isWordLevelFormat, parseQRCLyric, parseSmartLrc } from "@/utils/lyricParser";
1010
import { stripLyricMetadata } from "@/utils/lyricStripper";
1111
import { getConverter } from "@/utils/opencc";
1212
import { type LyricLine, parseLrc, parseTTML, parseYrc } from "@applemusic-like-lyrics/lyric";
@@ -36,6 +36,7 @@ class LyricManager {
3636
// 重置歌词数据
3737
musicStore.setSongLyric({}, true);
3838
statusStore.usingTTMLLyric = false;
39+
statusStore.usingQRCLyric = false;
3940
// 重置歌词索引
4041
statusStore.lyricIndex = -1;
4142
statusStore.lyricLoading = false;
@@ -95,17 +96,7 @@ class LyricManager {
9596
otherLyrics: LyricLine[],
9697
key: "translatedLyric" | "romanLyric",
9798
): LyricLine[] {
98-
const lyricsData = lyrics;
99-
if (lyricsData.length && otherLyrics.length) {
100-
lyricsData.forEach((v: LyricLine) => {
101-
otherLyrics.forEach((x: LyricLine) => {
102-
if (v.startTime === x.startTime || Math.abs(v.startTime - x.startTime) < 300) {
103-
v[key] = x.words.map((word) => word.word).join("");
104-
}
105-
});
106-
});
107-
}
108-
return lyricsData;
99+
return alignLyrics(lyrics, otherLyrics, key);
109100
}
110101

111102
/**
@@ -193,6 +184,7 @@ class LyricManager {
193184
if (durationDiff > 5000) {
194185
console.warn(
195186
`QQ 音乐歌词时长不匹配: ${data.song.duration}ms vs ${song.duration}ms (差异 ${durationDiff}ms)`,
187+
data,
196188
);
197189
return null;
198190
}
@@ -256,127 +248,7 @@ class LyricManager {
256248
* @returns LyricLine 数组
257249
*/
258250
private parseQRCLyric(qrcContent: string, trans?: string, roma?: string): LyricLine[] {
259-
// 行匹配: [开始时间,持续时间]内容
260-
const linePattern = /^\[(\d+),(\d+)\](.*)$/;
261-
// 逐字匹配: 文字(开始时间,持续时间)
262-
const wordPattern = /([^(]*)\((\d+),(\d+)\)/g;
263-
/**
264-
* 解析 QRC 内容为行数据
265-
*/
266-
const parseQRCContent = (
267-
rawContent: string,
268-
): Array<{
269-
startTime: number;
270-
endTime: number;
271-
words: Array<{ word: string; startTime: number; endTime: number }>;
272-
}> => {
273-
// 从 XML 中提取歌词内容
274-
const contentMatch = /<Lyric_1[^>]*LyricContent="([^"]*)"[^>]*\/>/.exec(rawContent);
275-
const content = contentMatch ? contentMatch[1] : rawContent;
276-
277-
const result: Array<{
278-
startTime: number;
279-
endTime: number;
280-
words: Array<{ word: string; startTime: number; endTime: number }>;
281-
}> = [];
282-
283-
for (const rawLine of content.split("\n")) {
284-
const line = rawLine.trim();
285-
if (!line) continue;
286-
287-
// 跳过元数据标签 [ti:xxx] [ar:xxx] 等
288-
if (/^\[[a-z]+:/i.test(line)) continue;
289-
290-
const lineMatch = linePattern.exec(line);
291-
if (!lineMatch) continue;
292-
293-
const lineStart = parseInt(lineMatch[1], 10);
294-
const lineDuration = parseInt(lineMatch[2], 10);
295-
const lineContent = lineMatch[3];
296-
297-
// 解析逐字
298-
const words: Array<{ word: string; startTime: number; endTime: number }> = [];
299-
let wordMatch: RegExpExecArray | null;
300-
const wordRegex = new RegExp(wordPattern.source, "g");
301-
302-
while ((wordMatch = wordRegex.exec(lineContent)) !== null) {
303-
const wordText = wordMatch[1];
304-
const wordStart = parseInt(wordMatch[2], 10);
305-
const wordDuration = parseInt(wordMatch[3], 10);
306-
307-
if (wordText) {
308-
words.push({
309-
word: wordText,
310-
startTime: wordStart,
311-
endTime: wordStart + wordDuration,
312-
});
313-
}
314-
}
315-
316-
if (words.length > 0) {
317-
result.push({
318-
startTime: lineStart,
319-
endTime: lineStart + lineDuration,
320-
words,
321-
});
322-
}
323-
}
324-
return result;
325-
};
326-
// 解析主歌词
327-
const qrcLines = parseQRCContent(qrcContent);
328-
let result = qrcLines.map((qrcLine) => {
329-
return {
330-
words: qrcLine.words.map((word) => ({
331-
...word,
332-
romanWord: "",
333-
})),
334-
startTime: qrcLine.startTime,
335-
endTime: qrcLine.endTime,
336-
translatedLyric: "",
337-
romanLyric: "",
338-
isBG: false,
339-
isDuet: false,
340-
};
341-
});
342-
// 处理翻译
343-
if (trans) {
344-
let transLines = parseLrc(trans);
345-
if (transLines?.length) {
346-
// 过滤包含 "//" 或 "作品的著作权" 的翻译行
347-
transLines = transLines.filter((line) => {
348-
const text = line.words.map((w) => w.word).join("");
349-
return !text.includes("//") && !text.includes("作品的著作权");
350-
});
351-
result = this.alignLyrics(result, transLines, "translatedLyric");
352-
}
353-
}
354-
// 处理音译
355-
if (roma) {
356-
const qrcRomaLines = parseQRCContent(roma);
357-
if (qrcRomaLines?.length) {
358-
const romaLines = qrcRomaLines.map((line) => {
359-
return {
360-
words: [
361-
{
362-
startTime: line.startTime,
363-
endTime: line.endTime,
364-
word: line.words.map((w) => w.word).join(""),
365-
romanWord: "",
366-
},
367-
],
368-
startTime: line.startTime,
369-
endTime: line.endTime,
370-
translatedLyric: "",
371-
romanLyric: "",
372-
isBG: false,
373-
isDuet: false,
374-
};
375-
});
376-
result = this.alignLyrics(result, romaLines, "romanLyric");
377-
}
378-
}
379-
return result;
251+
return parseQRCLyric(qrcContent, trans, roma);
380252
}
381253

382254
/**
@@ -505,6 +377,8 @@ class LyricManager {
505377
await Promise.allSettled([adoptTTML(), adoptLRC()]);
506378
// 优先使用 TTML
507379
statusStore.usingTTMLLyric = ttmlAdopted;
380+
// 设置是否使用 QRC 歌词(来自 QQ 音乐,且未被 TTML 覆盖)
381+
statusStore.usingQRCLyric = qqMusicAdopted && !ttmlAdopted;
508382
return await this.applyChineseVariant(this.handleLyricExclude(result));
509383
}
510384

@@ -560,6 +434,7 @@ class LyricManager {
560434
lrcData: aligned.lrcData,
561435
yrcData: qqLyric.yrcData,
562436
};
437+
statusStore.usingQRCLyric = true;
563438
}
564439
}
565440
return await this.applyChineseVariant(aligned);

src/core/player/SongManager.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { QualityType, type SongType } from "@/types/main";
1111
import { isLogin } from "@/utils/auth";
1212
import { isElectron } from "@/utils/env";
1313
import { formatSongsList } from "@/utils/format";
14+
import { AI_AUDIO_LEVELS } from "@/utils/meta";
1415
import { handleSongQuality } from "@/utils/helper";
1516
import { openUserLogin } from "@/utils/modal";
1617

@@ -120,7 +121,13 @@ class SongManager {
120121
*/
121122
public getOnlineUrl = async (id: number, isPc: boolean = false): Promise<AudioSource> => {
122123
const settingStore = useSettingStore();
123-
const level = isPc ? "exhigh" : settingStore.songLevel;
124+
let level = isPc ? "exhigh" : settingStore.songLevel;
125+
126+
// Fuck AI Mode: 如果开启,且请求的 level 是 AI 音质,降级为 hires
127+
if (settingStore.disableAiAudio && AI_AUDIO_LEVELS.includes(level)) {
128+
level = "hires";
129+
}
130+
124131
const res = await songUrl(id, level);
125132
console.log(`🌐 ${id} music data:`, res);
126133
const songData = res.data?.[0];

src/core/resource/DownloadManager.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { qqMusicMatch } from "@/api/qqmusic";
99
import { songLevelData } from "@/utils/meta";
1010
import { getPlayerInfoObj } from "@/utils/format";
1111
import { getConverter, type ConverterMode } from "@/utils/opencc";
12+
import { lyricLinesToTTML, parseQRCLyric } from "@/utils/lyricParser";
1213

1314
interface DownloadTask {
1415
song: SongType;
@@ -410,10 +411,25 @@ class DownloadManager {
410411
console.log(`[Download] Trying QM fallback with keyword: ${keyword}`);
411412
const qmResult = await qqMusicMatch(keyword);
412413
if (qmResult?.code === 200 && qmResult?.qrc) {
413-
yrcLyric = qmResult.qrc;
414-
console.log(
415-
`[Download] QM QRC fetched as fallback, len: ${yrcLyric?.length}`,
414+
// 解析 QRC 歌词(包含翻译和音译对齐)
415+
const parsedLines = parseQRCLyric(
416+
qmResult.qrc,
417+
qmResult.trans,
418+
qmResult.roma,
416419
);
420+
if (parsedLines.length > 0) {
421+
// 转换为 TTML 格式
422+
ttmlLyric = lyricLinesToTTML(parsedLines);
423+
console.log(
424+
`[Download] QM QRC parsed and converted to TTML, lines: ${parsedLines.length}`,
425+
);
426+
} else {
427+
// 如果解析失败,保留原始 QRC
428+
yrcLyric = qmResult.qrc;
429+
console.log(
430+
`[Download] QM QRC fetched as fallback (raw), len: ${yrcLyric?.length}`,
431+
);
432+
}
417433
}
418434
} catch (e) {
419435
console.error("[Download] Error fetching QM lyrics as fallback:", e);

src/stores/status.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ interface StatusState {
5656
pureLyricMode: boolean;
5757
/** 当前是否正使用 TTML 歌词 */
5858
usingTTMLLyric: boolean;
59+
/** 当前是否正使用 QRC 歌词(来自QQ音乐) */
60+
usingQRCLyric: boolean;
5961
/** 当前歌曲音质 */
6062
songQuality: QualityType | undefined;
6163
/** 当前播放索引 */
@@ -153,6 +155,7 @@ export const useStatusStore = defineStore("status", {
153155
songCoverTheme: {},
154156
pureLyricMode: false,
155157
usingTTMLLyric: false,
158+
usingQRCLyric: false,
156159
songQuality: undefined,
157160
playIndex: -1,
158161
lyricIndex: -1,

src/utils/helper.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -426,25 +426,14 @@ export const handleSongQuality = (
426426
"standard": QualityType.LQ,
427427
};
428428

429-
// Fuck AI Filter
430-
if (disableAiAudio && typeof song === "object" && song) {
431-
if ("level" in song) {
432-
if (AI_AUDIO_LEVELS.includes(song.level)) {
433-
return QualityType.HiRes;
434-
}
435-
}
436-
if ("privilege" in song) {
437-
const p = song.privilege;
438-
const level = p?.playMaxBrLevel ?? p?.plLevel;
439-
if (AI_AUDIO_LEVELS.includes(level)) {
440-
const quality = levelQualityMap["hires"];
441-
if (quality) return quality;
442-
}
443-
}
444-
}
429+
// Fuck AI Filter: 如果是 AI 音质,跳过 level 属性判断,让后续遍历逻辑来确定真正的最高音质
430+
const isAiLevel = disableAiAudio && typeof song === "object" && song && (
431+
("level" in song && AI_AUDIO_LEVELS.includes(song.level)) ||
432+
("privilege" in song && AI_AUDIO_LEVELS.includes(song.privilege?.playMaxBrLevel ?? song.privilege?.plLevel))
433+
);
445434

446-
if (typeof song === "object" && song) {
447-
// 含有 level 特殊处理
435+
if (typeof song === "object" && song && !isAiLevel) {
436+
// 含有 level 特殊处理(仅在非 AI 音质时使用)
448437
if ("level" in song) {
449438
const quality = levelQualityMap[song.level];
450439
if (quality) return quality;

0 commit comments

Comments
 (0)