lyrics: add translation lyrics for netease.ts (#951)

* lyrics: add translation lyrics for netease.ts
This commit is contained in:
et21ff 2025-06-22 03:19:23 +08:00 committed by GitHub
parent e3751229b6
commit ae41fe99bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 90 additions and 2 deletions

View file

@ -609,6 +609,8 @@
"mpvExtraParameters_help": "one per line", "mpvExtraParameters_help": "one per line",
"musicbrainz": "show musicbrainz links", "musicbrainz": "show musicbrainz links",
"musicbrainz_description": "show links to musicbrainz on artist/album pages, where mbid exists", "musicbrainz_description": "show links to musicbrainz on artist/album pages, where mbid exists",
"neteaseTranslation": "Enable NetEase translations",
"neteaseTranslation_description": "When enabled, fetches and displays translated lyrics from NetEase if available.",
"passwordStore": "passwords/secret store", "passwordStore": "passwords/secret store",
"passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords.", "passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords.",
"playbackStyle": "playback style", "playbackStyle": "playback style",

View file

@ -180,6 +180,8 @@
"followLyric_description": "滚动歌词到当前播放位置", "followLyric_description": "滚动歌词到当前播放位置",
"audioExclusiveMode": "音频独占模式", "audioExclusiveMode": "音频独占模式",
"font": "字体", "font": "字体",
"neteaseTranslation": "启用网易云歌词翻译",
"neteaseTranslation_description": "启用后,在获取歌词时将包含并显示网易云音乐提供的翻译(如果存在)。",
"crossfadeDuration_description": "设置淡入淡出持续时间", "crossfadeDuration_description": "设置淡入淡出持续时间",
"audioDevice": "音频设备", "audioDevice": "音频设备",
"enableRemote": "启用远程控制服务器", "enableRemote": "启用远程控制服务器",

View file

@ -6,6 +6,7 @@ import {
LyricSearchQuery, LyricSearchQuery,
LyricSource, LyricSource,
} from '.'; } from '.';
import { store } from '../settings';
import { orderSearchResults } from './shared'; import { orderSearchResults } from './shared';
const SEARCH_URL = 'https://music.163.com/api/search/get'; const SEARCH_URL = 'https://music.163.com/api/search/get';
@ -76,14 +77,20 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
id: songId, id: songId,
kv: '-1', kv: '-1',
lv: '-1', lv: '-1',
tv: '-1',
}, },
}); });
} catch (e) { } catch (e) {
console.error('NetEase lyrics request got an error!', e); console.error('NetEase lyrics request got an error!', e);
return null; return null;
} }
const enableTranslation = store.get('enableNeteaseTranslation', true) as boolean;
return result.data.klyric?.lyric || result.data.lrc?.lyric; const originalLrc = result.data.lrc?.lyric;
if (!enableTranslation) {
return originalLrc || null;
}
const translatedLrc = result.data.tlyric?.lyric;
return mergeLyrics(originalLrc, translatedLrc);
} }
export async function getSearchResults( export async function getSearchResults(
@ -166,3 +173,54 @@ async function getMatchedLyrics(
return firstMatch; return firstMatch;
} }
function mergeLyrics(original: string | undefined, translated: string | undefined): null | string {
if (!original) {
return null;
}
if (!translated) {
return original;
}
const lrcLineRegex = /\[(\d{2}:\d{2}\.\d{2,3})\](.*)/;
const translatedMap = new Map<string, string>();
// Parse the translated LRC and store it in a Map for efficient timestamp-based lookups.
translated.split('\n').forEach((line) => {
const match = line.match(lrcLineRegex);
if (match) {
const timestamp = match[1];
const text = match[2].trim();
if (text) {
translatedMap.set(timestamp, text);
}
}
});
if (translatedMap.size === 0) {
return original;
}
// Iterate through each line of the original LRC. If a translation exists for
// the same timestamp, insert it as a new, fully-formatted LRC line.
const finalLines = original.split('\n').flatMap((line) => {
const match = line.match(lrcLineRegex);
if (match) {
const timestamp = match[1];
const translatedText = translatedMap.get(timestamp);
if (translatedText) {
// Return an array containing both the original line and the new translated line.
// flatMap will flatten this into the final array of lines.
const translatedLine = `[${timestamp}]${translatedText}`;
return [line, translatedLine];
}
}
// If no match or no translation is found, return only the original line.
return [line];
});
return finalLines.join('\n');
}

View file

@ -101,6 +101,30 @@ export const LyricSettings = () => {
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.lyricFetchProvider', { postProcess: 'sentenceCase' }), title: t('setting.lyricFetchProvider', { postProcess: 'sentenceCase' }),
}, },
{
control: (
<Switch
aria-label="Enable NetEase translations"
defaultChecked={settings.enableNeteaseTranslation}
onChange={(e) => {
const isChecked = e.currentTarget.checked;
setSettings({
lyrics: {
...settings,
enableNeteaseTranslation: e.currentTarget.checked,
},
});
localSettings?.set('enableNeteaseTranslation', isChecked);
}}
/>
),
description: t('setting.neteaseTranslation', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.neteaseTranslation', { postProcess: 'sentenceCase' }),
},
{ {
control: ( control: (
<NumberInput <NumberInput

View file

@ -263,6 +263,7 @@ export interface SettingsState {
lyrics: { lyrics: {
alignment: 'center' | 'left' | 'right'; alignment: 'center' | 'left' | 'right';
delayMs: number; delayMs: number;
enableNeteaseTranslation: boolean;
fetch: boolean; fetch: boolean;
follow: boolean; follow: boolean;
fontSize: number; fontSize: number;
@ -445,6 +446,7 @@ const initialState: SettingsState = {
lyrics: { lyrics: {
alignment: 'center', alignment: 'center',
delayMs: 0, delayMs: 0,
enableNeteaseTranslation: false,
fetch: false, fetch: false,
follow: true, follow: true,
fontSize: 46, fontSize: 46,