From 7de9250aee52b01914dd2729ec0f4a1af3126143 Mon Sep 17 00:00:00 2001 From: Sergio Caguana Date: Sat, 25 Oct 2025 18:16:15 -0500 Subject: [PATCH 1/2] feat(lyrics): integrate SimpMusic Lyrics provider --- .../providers/SimpMusicLyrics.ts | 140 ++++++++++++++++++ src/plugins/synced-lyrics/providers/index.ts | 3 +- .../synced-lyrics/providers/renderer.ts | 2 + 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts diff --git a/src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts b/src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts new file mode 100644 index 0000000000..3d7504ff6a --- /dev/null +++ b/src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts @@ -0,0 +1,140 @@ +import { jaroWinkler } from '@skyra/jaro-winkler'; +import { config } from '../renderer/renderer'; +import { LRC } from '../parsers/lrc'; + +import type { LyricProvider, LyricResult, SearchSongInfo } from '../types'; + +export class SimpMusicLyrics implements LyricProvider { + name = 'SimpMusicLyrics'; + baseUrl = 'https://api-lyrics.simpmusic.org/v1'; + + async search({ + title, + alternativeTitle, + artist, + album, + songDuration, + tags, + }: SearchSongInfo): Promise { + let data: SimpMusicSong[] = []; + + let query = new URLSearchParams({ q: `${title} ${artist}` }); + let url = `${this.baseUrl}/search?${query.toString()}`; + let response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error (${response.statusText})`); + } + + let json = (await response.json()) as SimpMusicResponse; + data = json?.data ?? []; + + if (!data.length) { + query = new URLSearchParams({ q: title }); + url = `${this.baseUrl}/search?${query.toString()}`; + + response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error (${response.statusText})`); + } + + json = (await response.json()) as SimpMusicResponse; + data = json?.data ?? []; + } + + if (!data.length && alternativeTitle) { + query = new URLSearchParams({ q: alternativeTitle }); + url = `${this.baseUrl}/search?${query.toString()}`; + + response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error (${response.statusText})`); + } + + json = (await response.json()) as SimpMusicResponse; + data = json?.data ?? []; + } + + if (!Array.isArray(data) || data.length === 0) { + if (config()?.showLyricsEvenIfInexact) { + return null; + } + return null; + } + + const filteredResults: SimpMusicSong[] = []; + + for (const item of data) { + const { artistName } = item; + const artists = artist.split(/[&,]/g).map((i) => i.trim()); + const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim()); + + const permutations: [string, string][] = []; + for (const a of artists) { + for (const b of itemArtists) { + permutations.push([a.toLowerCase(), b.toLowerCase()]); + } + } + + const ratio = Math.max(...permutations.map(([x, y]) => jaroWinkler(x, y))); + if (ratio < 0.85) continue; + + filteredResults.push(item); + } + + if (!filteredResults.length) return null; + + filteredResults.sort( + (a, b) => + Math.abs(a.durationSeconds - songDuration) - + Math.abs(b.durationSeconds - songDuration), + ); + + const maxVote = Math.max(...filteredResults.map((r) => r.vote ?? 0)); + + const topVoted = filteredResults.filter((r) => (r.vote ?? 0) === maxVote); + + const best = topVoted[0]; + + if (!best) return null; + + if (Math.abs(best.durationSeconds - songDuration) > 15) { + return null; + } + + const raw = best.syncedLyrics; + const plain = best.plainLyric; + + if (!raw && !plain) return null; + + return { + title: best.songTitle, + artists: best.artistName.split(/[&,]/g).map((a) => a.trim()), + lines: raw + ? LRC.parse(raw).lines.map((l) => ({ + ...l, + status: 'upcoming' as const, + })) + : undefined, + lyrics: plain, + }; + } +} + +type SimpMusicResponse = { + type: string; + data: SimpMusicSong[]; + success?: boolean; +}; + +type SimpMusicSong = { + id: string; + videoId?: string; + songTitle: string; + artistName: string; + albumName?: string; + durationSeconds: number; + plainLyric?: string; + syncedLyrics?: string; + vote?: number; +}; \ No newline at end of file diff --git a/src/plugins/synced-lyrics/providers/index.ts b/src/plugins/synced-lyrics/providers/index.ts index 80413b0fe7..4e20e56177 100644 --- a/src/plugins/synced-lyrics/providers/index.ts +++ b/src/plugins/synced-lyrics/providers/index.ts @@ -7,6 +7,7 @@ export enum ProviderNames { LRCLib = 'LRCLib', MusixMatch = 'MusixMatch', LyricsGenius = 'LyricsGenius', + SimpMusicLyrics = 'SimpMusic Lyrics', // Megalobiz = 'Megalobiz', } @@ -18,4 +19,4 @@ export type ProviderState = { state: 'fetching' | 'done' | 'error'; data: LyricResult | null; error: Error | null; -}; +}; \ No newline at end of file diff --git a/src/plugins/synced-lyrics/providers/renderer.ts b/src/plugins/synced-lyrics/providers/renderer.ts index 77f16c9608..027b9765ab 100644 --- a/src/plugins/synced-lyrics/providers/renderer.ts +++ b/src/plugins/synced-lyrics/providers/renderer.ts @@ -3,11 +3,13 @@ import { YTMusic } from './YTMusic'; import { LRCLib } from './LRCLib'; import { MusixMatch } from './MusixMatch'; import { LyricsGenius } from './LyricsGenius'; +import { SimpMusicLyrics } from './SimpMusicLyrics'; export const providers = { [ProviderNames.YTMusic]: new YTMusic(), [ProviderNames.LRCLib]: new LRCLib(), [ProviderNames.MusixMatch]: new MusixMatch(), [ProviderNames.LyricsGenius]: new LyricsGenius(), + [ProviderNames.SimpMusicLyrics]: new SimpMusicLyrics(), // [ProviderNames.Megalobiz]: new Megalobiz(), // Disabled because it is too unstable and slow } as const; From 1aa014eda6aace1cba55139e8eda973239523437 Mon Sep 17 00:00:00 2001 From: Sergio Caguana Date: Sat, 25 Oct 2025 19:13:46 -0500 Subject: [PATCH 2/2] fix(prettier): Fix code style --- src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts | 9 +++++---- src/plugins/synced-lyrics/providers/index.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts b/src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts index 3d7504ff6a..b788c7e975 100644 --- a/src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts +++ b/src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts @@ -1,4 +1,5 @@ import { jaroWinkler } from '@skyra/jaro-winkler'; + import { config } from '../renderer/renderer'; import { LRC } from '../parsers/lrc'; @@ -12,9 +13,7 @@ export class SimpMusicLyrics implements LyricProvider { title, alternativeTitle, artist, - album, songDuration, - tags, }: SearchSongInfo): Promise { let data: SimpMusicSong[] = []; @@ -76,7 +75,9 @@ export class SimpMusicLyrics implements LyricProvider { } } - const ratio = Math.max(...permutations.map(([x, y]) => jaroWinkler(x, y))); + const ratio = Math.max( + ...permutations.map(([x, y]) => jaroWinkler(x, y)), + ); if (ratio < 0.85) continue; filteredResults.push(item); @@ -137,4 +138,4 @@ type SimpMusicSong = { plainLyric?: string; syncedLyrics?: string; vote?: number; -}; \ No newline at end of file +}; diff --git a/src/plugins/synced-lyrics/providers/index.ts b/src/plugins/synced-lyrics/providers/index.ts index 4e20e56177..0e915c0f82 100644 --- a/src/plugins/synced-lyrics/providers/index.ts +++ b/src/plugins/synced-lyrics/providers/index.ts @@ -19,4 +19,4 @@ export type ProviderState = { state: 'fetching' | 'done' | 'error'; data: LyricResult | null; error: Error | null; -}; \ No newline at end of file +};