Add support for OpenSubsonic enhanced lyrics (#2208)

This commit is contained in:
Jeff
2026-07-09 21:32:05 -07:00
committed by GitHub
parent 0c89fea1ea
commit 355b19c1cb
55 changed files with 4213 additions and 480 deletions
+92
View File
@@ -1,8 +1,19 @@
import Kuroshiro from 'kuroshiro';
import KuromojiAnalyzer from 'kuroshiro-analyzer-kuromoji';
import { hasJapanese, kanaToRomaji, patchTokens } from 'kuroshiro/lib/util';
// doc: https://kuroshiro.org
export type LyricTextToken = {
endChar: number;
startChar: number;
text: string;
};
export type RomajiToken = LyricTextToken & {
romaji: string;
};
let kuroshiroInstance: any = null;
let initPromise: null | Promise<void> = null;
@@ -39,6 +50,20 @@ export const convertFurigana = async (text: string): Promise<string> => {
}
};
export const convertFuriganaFragment = async (text: string): Promise<string> => {
if (!text || !hasJapanese(text)) {
return text;
}
try {
const kuroshiro = await getKuroshiro();
return await kuroshiro.convert(text, { mode: 'furigana', to: 'hiragana' });
} catch (e) {
console.error('Furigana fragment conversion error: ', e);
return text;
}
};
export const convertRomaji = async (text: string): Promise<string> => {
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
@@ -52,3 +77,70 @@ export const convertRomaji = async (text: string): Promise<string> => {
return '';
}
};
export const parseLyricsTextTokens = async (text: string): Promise<LyricTextToken[]> => {
if (!text || !hasJapanese(text)) {
return [];
}
try {
const kuroshiro = await getKuroshiro();
const rawTokens = await kuroshiro._analyzer.parse(text);
const tokens = patchTokens(rawTokens);
let cursor = 0;
return tokens.map((token: { surface_form: string }) => {
const surface = token.surface_form;
const startChar = cursor;
cursor += surface.length;
return {
endChar: cursor,
startChar,
text: surface,
};
});
} catch (e) {
console.error('Lyrics token parse error: ', e);
return [];
}
};
export const convertRomajiTokens = async (text: string): Promise<RomajiToken[]> => {
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
if (!KuroshiroClass.Util.hasKana(text)) {
return [];
}
try {
const kuroshiro = await getKuroshiro();
const rawTokens = await kuroshiro._analyzer.parse(text);
const tokens = patchTokens(rawTokens);
let cursor = 0;
return tokens.map(
(token: { pronunciation?: string; reading: string; surface_form: string }) => {
const surface = token.surface_form;
const startChar = cursor;
cursor += surface.length;
const romaji = hasJapanese(surface)
? kanaToRomaji(token.pronunciation || token.reading)
: surface;
return {
endChar: cursor,
romaji,
startChar,
text: surface,
};
},
);
} catch (e) {
console.error('Romaji token conversion error: ', e);
return [];
}
};
+19 -1
View File
@@ -1,7 +1,13 @@
import { ipcMain } from 'electron';
import { store } from '../settings';
import { convertFurigana, convertRomaji } from './furigana';
import {
convertFurigana,
convertFuriganaFragment,
convertRomaji,
convertRomajiTokens,
parseLyricsTextTokens,
} from './furigana';
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease';
@@ -237,6 +243,18 @@ ipcMain.handle('lyric-convert-furigana', async (_event, text: string) => {
return await convertFurigana(text);
});
ipcMain.handle('lyric-convert-furigana-fragment', async (_event, text: string) => {
return await convertFuriganaFragment(text);
});
ipcMain.handle('lyric-parse-text-tokens', async (_event, text: string) => {
return await parseLyricsTextTokens(text);
});
ipcMain.handle('lyric-convert-romaji', async (_event, text: string) => {
return await convertRomaji(text);
});
ipcMain.handle('lyric-convert-romaji-tokens', async (_event, text: string) => {
return await convertRomajiTokens(text);
});