Skip to content

Commit fe5dfcd

Browse files
authored
Merge pull request #612 from Txt-Text/dev-dl
✨ feat: 增加下载嵌入翻译和音译功能
2 parents a7a6108 + c01bd16 commit fe5dfcd

3 files changed

Lines changed: 184 additions & 37 deletions

File tree

src/components/Setting/LocalSetting.vue

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,30 @@
155155
class="set"
156156
/>
157157
</n-card>
158+
<n-card class="set-item">
159+
<div class="label">
160+
<n-text class="name">同时下载歌词翻译</n-text>
161+
<n-text class="tip" :depth="3">下载歌词时同时包含翻译</n-text>
162+
</div>
163+
<n-switch
164+
v-model:value="settingStore.downloadLyricTranslation"
165+
:disabled="!settingStore.downloadMeta || !settingStore.downloadLyric"
166+
:round="false"
167+
class="set"
168+
/>
169+
</n-card>
170+
<n-card class="set-item">
171+
<div class="label">
172+
<n-text class="name">同时下载歌词音译</n-text>
173+
<n-text class="tip" :depth="3">下载歌词时同时包含音译(罗马音)</n-text>
174+
</div>
175+
<n-switch
176+
v-model:value="settingStore.downloadLyricRomaji"
177+
:disabled="!settingStore.downloadMeta || !settingStore.downloadLyric"
178+
:round="false"
179+
class="set"
180+
/>
181+
</n-card>
158182
<n-card class="set-item">
159183
<div class="label">
160184
<n-text class="name">音乐命名格式</n-text>

src/stores/setting.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ export interface SettingState {
88
themeMode: "light" | "dark" | "auto";
99
/** 主题类别 */
1010
themeColorType:
11-
| "default"
12-
| "orange"
13-
| "blue"
14-
| "pink"
15-
| "brown"
16-
| "indigo"
17-
| "green"
18-
| "purple"
19-
| "yellow"
20-
| "teal"
21-
| "custom";
11+
| "default"
12+
| "orange"
13+
| "blue"
14+
| "pink"
15+
| "brown"
16+
| "indigo"
17+
| "green"
18+
| "purple"
19+
| "yellow"
20+
| "teal"
21+
| "custom";
2222
/** 主题自定义颜色 */
2323
themeCustomColor: string;
2424
/** 全局着色 */
@@ -95,14 +95,14 @@ export interface SettingState {
9595
proxyPort: number;
9696
/** 歌曲音质 */
9797
songLevel:
98-
| "standard"
99-
| "higher"
100-
| "exhigh"
101-
| "lossless"
102-
| "hires"
103-
| "jyeffect"
104-
| "sky"
105-
| "jymaster";
98+
| "standard"
99+
| "higher"
100+
| "exhigh"
101+
| "lossless"
102+
| "hires"
103+
| "jyeffect"
104+
| "sky"
105+
| "jymaster";
106106
/** 播放设备 */
107107
playDevice: "default" | string;
108108
/** 自动播放 */
@@ -321,7 +321,7 @@ export const useSettingStore = defineStore("setting", {
321321
downloadCover: true,
322322
downloadLyric: true,
323323
downloadLyricTranslation: true,
324-
downloadLyricRomaji: true,
324+
downloadLyricRomaji: false,
325325
usePlaybackForDownload: false,
326326
saveMetaFile: false,
327327
downloadSongLevel: "h",
@@ -386,12 +386,11 @@ export const useSettingStore = defineStore("setting", {
386386
}
387387
window.$message.info(
388388
`已切换至
389-
${
390-
this.themeMode === "auto"
391-
? "跟随系统"
392-
: this.themeMode === "light"
393-
? "浅色模式"
394-
: "深色模式"
389+
${this.themeMode === "auto"
390+
? "跟随系统"
391+
: this.themeMode === "light"
392+
? "浅色模式"
393+
: "深色模式"
395394
}`,
396395
{
397396
showIcon: false,

src/utils/downloadManager.ts

Lines changed: 135 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class DownloadManager {
1717
private queue: DownloadTask[] = [];
1818
private isProcessing: boolean = false;
1919

20-
private constructor() { }
20+
private constructor() {}
2121

2222
public static getInstance(): DownloadManager {
2323
if (!DownloadManager.instance) {
@@ -211,12 +211,11 @@ class DownloadManager {
211211
type = result.data.type?.toLowerCase() || "mp3";
212212
}
213213

214-
const infoObj =
215-
songManager.getPlayerInfoObj(song) || {
216-
name: song.name || "未知歌曲",
217-
artist: "未知歌手",
218-
album: "未知专辑",
219-
};
214+
const infoObj = songManager.getPlayerInfoObj(song) || {
215+
name: song.name || "未知歌曲",
216+
artist: "未知歌手",
217+
album: "未知专辑",
218+
};
220219

221220
const baseTitle = infoObj.name || "未知歌曲";
222221
const rawArtist = infoObj.artist || "未知歌手";
@@ -256,14 +255,139 @@ class DownloadManager {
256255
let lyric = "";
257256
if (downloadLyric) {
258257
const lyricResult = await songLyric(song.id);
259-
const rawLyric = lyricResult?.lrc?.lyric || "";
258+
260259
// 排除特定格式的脏数据
261-
const excludeRegex =
262-
/^\{"t":\d+,"c":\[\{"[^"]+":\"[^"]*\"}(?:,\{"[^"]+":\"[^"]*\"})*]}$/;
260+
const rawLyric = lyricResult?.lrc?.lyric || "";
261+
const excludeRegex = /^\{"t":\d+,"c":\[\{"[^"]+":"[^"]*"}(?:,\{"[^"]+":"[^"]*"})*]}$/;
263262
lyric = rawLyric
264263
.split("\n")
265-
.filter((line) => !excludeRegex.test(line.trim()))
264+
.filter((line: string) => !excludeRegex.test(line.trim()))
266265
.join("\n");
266+
267+
const lrc = lyricResult?.lrc?.lyric;
268+
const tlyric = settingStore.downloadLyricTranslation ? lyricResult?.tlyric?.lyric : null;
269+
const romalrc = settingStore.downloadLyricRomaji ? lyricResult?.romalrc?.lyric : null;
270+
271+
if (lrc) {
272+
// 正则:匹配 [mm:ss.xx] 或 [mm:ss.xxx] 形式的时间标签
273+
const timeTagRe = /\[(\d{2}):(\d{2})(?:\.(\d{1,3}))?\]/g;
274+
275+
// 把时间字符串转成秒(用于模糊匹配),例如 "00:06.52" -> 6.52
276+
const timeStrToSeconds = (timeStr: string) => {
277+
// timeStr 形如 "00:06.52" 或 "00:06"(不带小数)
278+
const m = timeStr.match(/^(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/);
279+
if (!m) return 0;
280+
const minutes = parseInt(m[1], 10);
281+
const seconds = parseInt(m[2], 10);
282+
const frac = m[3] ? parseInt((m[3] + "00").slice(0, 3), 10) : 0; // 保证到毫秒位
283+
return minutes * 60 + seconds + frac / 1000;
284+
};
285+
286+
/**
287+
* 解析歌词字符串为映射表
288+
* @param lyricStr 歌词字符串
289+
* @returns 映射表
290+
*/
291+
const parseToMap = (lyricStr: string) => {
292+
const map = new Map<string, string>();
293+
if (!lyricStr) return map;
294+
const lines = lyricStr.split(/\r?\n/);
295+
for (const raw of lines) {
296+
let m: RegExpExecArray | null;
297+
const timeTags: string[] = [];
298+
timeTagRe.lastIndex = 0;
299+
while ((m = timeTagRe.exec(raw)) !== null) {
300+
const frac = m[3] ?? "";
301+
const tag = `[${m[1]}:${m[2]}${frac ? "." + frac : ""}]`;
302+
timeTags.push(tag);
303+
}
304+
const text = raw.replace(timeTagRe, "").trim();
305+
for (const tag of timeTags) {
306+
if (text) {
307+
const prev = map.get(tag);
308+
map.set(tag, prev ? prev + "\n" + text : text);
309+
}
310+
}
311+
}
312+
return map;
313+
};
314+
315+
/**
316+
* 查找匹配文本(精确或模糊)
317+
* @param map 映射表
318+
* @param currentTag 当前时间标签
319+
* @returns 匹配文本
320+
*/
321+
const findMatch = (map: Map<string, string>, currentTag: string) => {
322+
// 精确匹配
323+
const exact = map.get(currentTag);
324+
if (exact) return exact;
325+
326+
// 模糊匹配
327+
const tSec = timeStrToSeconds(currentTag.slice(1, -1));
328+
let bestTag: string | null = null;
329+
let bestDiff = Infinity;
330+
for (const key of Array.from(map.keys())) {
331+
const kSec = timeStrToSeconds(key.slice(1, -1));
332+
const diff = Math.abs(kSec - tSec);
333+
if (diff < bestDiff) {
334+
bestDiff = diff;
335+
bestTag = key;
336+
}
337+
}
338+
if (bestTag && bestDiff < 0.5) {
339+
return map.get(bestTag);
340+
}
341+
return null;
342+
};
343+
344+
// 解析翻译和音译歌词
345+
const tMap = parseToMap(tlyric || "");
346+
const rMap = parseToMap(romalrc || "");
347+
348+
const lines: string[] = [];
349+
350+
// 逐行解析原始 lrc 字符串
351+
const lrcLinesRaw = lrc.split(/\r?\n/);
352+
for (const raw of lrcLinesRaw) {
353+
let m: RegExpExecArray | null;
354+
const timeTags: string[] = [];
355+
timeTagRe.lastIndex = 0;
356+
while ((m = timeTagRe.exec(raw)) !== null) {
357+
const frac = m[3] ?? "";
358+
const tag = `[${m[1]}:${m[2]}${frac ? "." + frac : ""}]`;
359+
timeTags.push(tag);
360+
}
361+
362+
if (timeTags.length === 0) continue;
363+
364+
const text = raw.replace(timeTagRe, "").trim();
365+
if (!text) continue;
366+
367+
for (const timeTag of timeTags) {
368+
lines.push(`${timeTag}${text}`);
369+
370+
// 添加翻译和音译歌词
371+
const lyricMaps = [
372+
{ map: tMap, enabled: tlyric },
373+
{ map: rMap, enabled: romalrc },
374+
];
375+
376+
for (const { map, enabled } of lyricMaps) {
377+
if (enabled) {
378+
const matchedText = findMatch(map, timeTag);
379+
if (matchedText) {
380+
for (const lt of matchedText.split("\n")) {
381+
if (lt.trim()) lines.push(`${timeTag}${lt}`);
382+
}
383+
}
384+
}
385+
}
386+
}
387+
}
388+
389+
lyric = lines.join("\n");
390+
}
267391
}
268392

269393
const config = {

0 commit comments

Comments
 (0)