diff --git a/src/main/features/core/lyrics/index.ts b/src/main/features/core/lyrics/index.ts index ffc5ad15f..28dd11c17 100644 --- a/src/main/features/core/lyrics/index.ts +++ b/src/main/features/core/lyrics/index.ts @@ -5,6 +5,10 @@ import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib'; import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease'; import { orderSearchResults } from './shared'; +import { + getLyricsBySongId as getSimpMusic, + getSearchResults as searchSimpMusic, +} from './simpmusic'; import { Song } from '/@/shared/types/domain-types'; @@ -12,6 +16,7 @@ export enum LyricSource { GENIUS = 'Genius', LRCLIB = 'lrclib.net', NETEASE = 'NetEase', + SIMPMUSIC = 'SimpMusic', } export type FullLyricsMetadata = Omit & { @@ -66,12 +71,14 @@ const SEARCH_FETCHERS: Record = { [LyricSource.GENIUS]: searchGenius, [LyricSource.LRCLIB]: searchLrcLib, [LyricSource.NETEASE]: searchNetease, + [LyricSource.SIMPMUSIC]: searchSimpMusic, }; const GET_FETCHERS: Record = { [LyricSource.GENIUS]: getGenius, [LyricSource.LRCLIB]: getLrcLib, [LyricSource.NETEASE]: getNetease, + [LyricSource.SIMPMUSIC]: getSimpMusic, }; const MAX_CACHED_ITEMS = 10; @@ -191,6 +198,7 @@ const searchRemoteLyrics = async (params: LyricSearchQuery) => { [LyricSource.GENIUS]: [], [LyricSource.LRCLIB]: [], [LyricSource.NETEASE]: [], + [LyricSource.SIMPMUSIC]: [], }; for (const item of allSearchResults) { results[item.source].push(item); diff --git a/src/main/features/core/lyrics/simpmusic.ts b/src/main/features/core/lyrics/simpmusic.ts new file mode 100644 index 000000000..a85ecf0b8 --- /dev/null +++ b/src/main/features/core/lyrics/simpmusic.ts @@ -0,0 +1,126 @@ +import axios, { AxiosResponse } from 'axios'; + +import { + InternetProviderLyricResponse, + InternetProviderLyricSearchResponse, + LyricSearchQuery, + LyricSource, +} from '.'; +import { orderSearchResults } from './shared'; + +const API_URL = 'https://api-lyrics.simpmusic.org/v1'; + +const TIMEOUT_MS = 5000; + +export interface SimpMusicLyric { + albumName?: string; + artistName: string; + durationSeconds?: number; + id: string; + plainLyric?: string; + richSyncLyrics?: string; + songTitle: string; + syncedLyrics?: string; + videoId: string; + vote?: number; +} + +export interface SimpMusicSearchResponse { + data: SimpMusicLyric[]; + success: boolean; +} + +export async function getLyricsBySongId(songId: string): Promise { + let result: AxiosResponse; + + try { + result = await axios.get(`${API_URL}/${songId}`, { + timeout: TIMEOUT_MS, + }); + } catch (e) { + console.error('SimpMusic lyrics request errored:', (e as Error)?.message); + return null; + } + + const firstLyric = (result.data.data?.[0] ?? null) as null | SimpMusicLyric; + if (!firstLyric) return null; + + return firstLyric.syncedLyrics || firstLyric.plainLyric || null; +} + +export async function getSearchResults( + params: LyricSearchQuery, +): Promise { + let result: AxiosResponse; + + if (!params.name) return null; + + try { + result = await axios.get(`${API_URL}/search`, { + params: { + q: params.name, + }, + timeout: TIMEOUT_MS, + }); + } catch (e) { + console.error('SimpMusic search errored:', (e as Error)?.message); + return null; + } + + if (!result.data?.data) return null; + + const songResults: InternetProviderLyricSearchResponse[] = result.data.data.map((song) => ({ + artist: song.artistName, + id: song.videoId, + isSync: song.syncedLyrics ? true : false, + name: song.songTitle, + source: LyricSource.SIMPMUSIC, + })); + + return orderSearchResults({ params, results: songResults }); +} + +export async function query( + params: LyricSearchQuery, +): Promise { + if (!params.name) return null; + + let search: AxiosResponse; + + try { + search = await axios.get(`${API_URL}/search`, { + params: { + q: params.name, + }, + timeout: TIMEOUT_MS, + }); + } catch (e) { + console.error('SimpMusic search errored:', (e as Error).message); + return null; + } + + const first = search.data?.data?.[0]; + if (!first) return null; + + let lyric: AxiosResponse; + + try { + lyric = await axios.get(`${API_URL}/${first.videoId}`, { + timeout: TIMEOUT_MS, + }); + } catch (e) { + console.error('SimpMusic lyrics fetch errored:', (e as Error).message); + return null; + } + + const lyrics = lyric.data.syncedLyrics || lyric.data.plainLyric || null; + if (!lyrics) return null; + + return { + artist: lyric.data.artistName, + id: lyric.data.videoId, + lyrics, + name: lyric.data.songTitle, + source: LyricSource.SIMPMUSIC, + }; +} diff --git a/src/renderer/features/analytics/hooks/use-app-tracker.ts b/src/renderer/features/analytics/hooks/use-app-tracker.ts index 96bd64a63..218814995 100644 --- a/src/renderer/features/analytics/hooks/use-app-tracker.ts +++ b/src/renderer/features/analytics/hooks/use-app-tracker.ts @@ -167,6 +167,9 @@ const getSettingsProperties = (): SettingsProperties => { 'settings.lyrics.sources.netease': ignoreWeb( settings.lyrics.sources.includes(LyricSource.NETEASE), ), + 'settings.lyrics.sources.simpmusic': ignoreWeb( + settings.lyrics.sources.includes(LyricSource.SIMPMUSIC), + ), 'settings.minimizeToTray': ignoreWeb(settings.window.minimizeToTray), // 'settings.musicBrainz': settings.general.musicBrainz, 'settings.nativeAspectRatio': settings.general.nativeAspectRatio, diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 87d8dd10e..6db45a0dd 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -1346,6 +1346,7 @@ export enum LyricSource { GENIUS = 'Genius', LRCLIB = 'lrclib.net', NETEASE = 'NetEase', + SIMPMUSIC = 'SimpMusic', } export type AlbumRadioArgs = BaseEndpointArgs & {