diff --git a/src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts b/src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts new file mode 100644 index 0000000000..b788c7e975 --- /dev/null +++ b/src/plugins/synced-lyrics/providers/SimpMusicLyrics.ts @@ -0,0 +1,141 @@ +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, + songDuration, + }: 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; +}; diff --git a/src/plugins/synced-lyrics/providers/index.ts b/src/plugins/synced-lyrics/providers/index.ts index 80413b0fe7..0e915c0f82 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', } 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;