diff --git a/src/main/features/core/lyrics/genius.ts b/src/main/features/core/lyrics/genius.ts index 1abaa3cd..12bad30c 100644 --- a/src/main/features/core/lyrics/genius.ts +++ b/src/main/features/core/lyrics/genius.ts @@ -1,6 +1,10 @@ import axios, { AxiosResponse } from 'axios'; import { load } from 'cheerio'; -import type { InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types'; +import type { + InternetProviderLyricResponse, + InternetProviderLyricSearchResponse, + LyricSearchQuery, +} from '/@/renderer/api/types'; import { LyricSource } from '../../../../renderer/types'; const SEARCH_URL = 'https://genius.com/api/search/song'; @@ -13,13 +17,86 @@ interface GeniusResponse { url: string; } -async function getSongURL(metadata: QueueSong): Promise { - let result: AxiosResponse; +interface GeniusSearchResponse { + response: { + sections: { + hits: { + highlights: any[]; + index: string; + result: { + _type: string; + annotation_count: number; + api_path: string; + artist_names: string; + featured_artits: any[]; + full_title: string; + header_image_thumbnail_url: string; + header_image_url: string; + id: number; + instrumental: boolean; + language: string; + lyrics_owner_id: number; + lyrics_state: string; + lyrics_updated_at: number; + path: string; + primary_artist: Record; + pyongs_count: number; + relationships_index_url: string; + release_date_components: Record; + release_date_for_display: string; + release_date_with_abbreviated_month_for_display: string; + song_art_image_thumbnail_url: string; + song_art_image_url: string; + stats: Record; + title: string; + title_with_featured: string; + updated_by_human_at: number; + url: string; + }; + type: string; + }[]; + type: string; + }[]; + }; +} + +export async function getSearchResults( + params: LyricSearchQuery, +): Promise { + let result: AxiosResponse; + try { + result = await axios.get(SEARCH_URL, { + params: { + per_page: '5', + q: `${params.artist} ${params.name}`, + }, + }); + } catch (e) { + console.error('Genius search request got an error!', e); + return null; + } + + const songs = result.data.response?.sections?.[0]?.hits?.map((hit) => hit.result); + + if (!songs) return null; + + return songs.map((song: any) => { + return { + artist: song.artist_names, + id: song.url, + name: song.full_title, + source: LyricSource.GENIUS, + }; + }); +} + +async function getSongURL(params: LyricSearchQuery): Promise { + let result: AxiosResponse; try { result = await axios.get(SEARCH_URL, { params: { per_page: '1', - q: `${metadata.artistName} ${metadata.name}`, + q: `${params.artist} ${params.name}`, }, }); } catch (e) { @@ -61,8 +138,10 @@ async function getLyricsFromGenius(url: string): Promise { return lyricSections; } -export async function query(metadata: QueueSong): Promise { - const response = await getSongURL(metadata); +export async function query( + params: LyricSearchQuery, +): Promise { + const response = await getSongURL(params); if (!response) { console.error('Could not find the song on Genius!'); return null; diff --git a/src/main/features/core/lyrics/index.ts b/src/main/features/core/lyrics/index.ts index 0927da2e..28e4d74a 100644 --- a/src/main/features/core/lyrics/index.ts +++ b/src/main/features/core/lyrics/index.ts @@ -1,11 +1,19 @@ -import { InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types'; -import { query as queryGenius } from './genius'; -import { query as queryNetease } from './netease'; +import { + InternetProviderLyricResponse, + InternetProviderLyricSearchResponse, + LyricSearchQuery, + QueueSong, +} from '/@/renderer/api/types'; +import { query as queryGenius, getSearchResults as searchGenius } from './genius'; +import { query as queryNetease, getSearchResults as searchNetease } from './netease'; import { LyricSource } from '../../../../renderer/types'; import { ipcMain } from 'electron'; import { store } from '../settings/index'; -type SongFetcher = (song: QueueSong) => Promise; +type SongFetcher = (params: LyricSearchQuery) => Promise; +type SearchFetcher = ( + params: LyricSearchQuery, +) => Promise; type CachedLyrics = Record; @@ -14,6 +22,11 @@ const FETCHERS: Record = { [LyricSource.NETEASE]: queryNetease, }; +const SEARCH_FETCHERS: Record = { + [LyricSource.GENIUS]: searchGenius, + [LyricSource.NETEASE]: searchNetease, +}; + const MAX_CACHED_ITEMS = 10; const lyricCache = new Map(); @@ -33,7 +46,9 @@ const getRemoteLyrics = async (song: QueueSong) => { let lyricsFromSource = null; for (const source of sources) { - const response = await FETCHERS[source](song); + const params = { artist: song.artistName, name: song.name }; + const response = await FETCHERS[source](params); + if (response) { const newResult = cached ? { @@ -57,7 +72,33 @@ const getRemoteLyrics = async (song: QueueSong) => { return lyricsFromSource; }; +const searchRemoteLyrics = async (params: LyricSearchQuery) => { + const sources = store.get('lyrics', []) as LyricSource[]; + + const results: Record = { + [LyricSource.GENIUS]: [], + [LyricSource.NETEASE]: [], + }; + + for (const source of sources) { + const response = await SEARCH_FETCHERS[source](params); + + if (response) { + response.forEach((result) => { + results[source].push(result); + }); + } + } + + return results; +}; + ipcMain.handle('lyric-fetch-manual', async (_event, song: QueueSong) => { const lyric = await getRemoteLyrics(song); return lyric; }); + +ipcMain.handle('lyric-search', async (_event, params: LyricSearchQuery) => { + const lyricResults = await searchRemoteLyrics(params); + return lyricResults; +}); diff --git a/src/main/features/core/lyrics/netease.ts b/src/main/features/core/lyrics/netease.ts index 40dd819e..bba00c10 100644 --- a/src/main/features/core/lyrics/netease.ts +++ b/src/main/features/core/lyrics/netease.ts @@ -1,6 +1,10 @@ import axios, { AxiosResponse } from 'axios'; -import type { InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types'; import { LyricSource } from '../../../../renderer/types'; +import type { + InternetProviderLyricResponse, + InternetProviderLyricSearchResponse, + LyricSearchQuery, +} from '/@/renderer/api/types'; const SEARCH_URL = 'https://music.163.com/api/search/get'; const LYRICS_URL = 'https://music.163.com/api/song/lyric'; @@ -13,30 +17,48 @@ interface NetEaseResponse { name: string; } -async function getSongId(metadata: QueueSong): Promise { +export async function getSearchResults( + params: LyricSearchQuery, +): Promise { let result: AxiosResponse; try { result = await axios.get(SEARCH_URL, { params: { - limit: 10, + limit: 5, offset: 0, - s: `${metadata.artistName} ${metadata.name}`, + s: `${params.artist} ${params.name}`, type: '1', }, }); } catch (e) { console.error('NetEase search request got an error!', e); - return undefined; + return null; } - const song = result?.data.result?.songs?.[0]; + const songs = result?.data.result?.songs; + + if (!songs) return null; + + return songs.map((song: any) => { + const artist = song.artists ? song.artists.map((artist: any) => artist.name).join(', ') : ''; + + return { + artist, + id: song.id, + name: song.name, + source: LyricSource.NETEASE, + }; + }); +} + +async function getSongId(params: LyricSearchQuery): Promise { + const results = await getSearchResults(params); + const song = results?.[0]; if (!song) return undefined; - const artist = song.artists ? song.artists.map((artist: any) => artist.name).join(', ') : ''; - return { - artist, + artist: song.artist, id: song.id, name: song.name, }; @@ -60,8 +82,10 @@ async function getLyricsFromSongId(songId: string): Promise return result.data.klyric?.lyric || result.data.lrc?.lyric; } -export async function query(metadata: QueueSong): Promise { - const response = await getSongId(metadata); +export async function query( + params: LyricSearchQuery, +): Promise { + const response = await getSongId(params); if (!response) { console.error('Could not find the song on NetEase!'); return null; diff --git a/src/main/preload/lyrics.ts b/src/main/preload/lyrics.ts index d0c25f38..6bafaf81 100644 --- a/src/main/preload/lyrics.ts +++ b/src/main/preload/lyrics.ts @@ -1,11 +1,16 @@ import { IpcRendererEvent, ipcRenderer } from 'electron'; -import { InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types'; +import { InternetProviderLyricResponse, LyricSearchQuery, QueueSong } from '/@/renderer/api/types'; const fetchRemoteLyrics = (song: QueueSong) => { const result = ipcRenderer.invoke('lyric-fetch-manual', song); return result; }; +const searchRemoteLyrics = (params: LyricSearchQuery) => { + const result = ipcRenderer.invoke('lyric-search', params); + return result; +}; + const remoteLyricsListener = ( cb: ( event: IpcRendererEvent, @@ -20,4 +25,5 @@ const remoteLyricsListener = ( export const lyrics = { fetchRemoteLyrics, remoteLyricsListener, + searchRemoteLyrics, }; diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index c22ac635..db53c3ec 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -15,6 +15,7 @@ import type { SongDetailQuery, RandomSongListQuery, LyricsQuery, + LyricSearchQuery, } from './types'; export const queryKeys: Record< @@ -107,6 +108,10 @@ export const queryKeys: Record< if (query) return [serverId, 'song', 'lyrics', query] as const; return [serverId, 'song', 'lyrics'] as const; }, + lyricsSearch: (query?: LyricSearchQuery) => { + if (query) return ['lyrics', 'search', query] as const; + return ['lyrics', 'search'] as const; + }, randomSongList: (serverId: string, query?: RandomSongListQuery) => { if (query) return [serverId, 'songs', 'randomSongList', query] as const; return [serverId, 'songs', 'randomSongList'] as const; diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index c6aa0c78..65ed4f59 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -1036,6 +1036,13 @@ export type InternetProviderLyricResponse = { source: string; }; +export type InternetProviderLyricSearchResponse = { + artist: string; + id: string; + name: string; + source: string; +}; + export type SynchronizedLyricMetadata = { lyrics: SynchronizedLyricsArray; remote: boolean; @@ -1053,3 +1060,8 @@ export type LyricOverride = Omit; export const instanceOfCancellationError = (error: any) => { return 'revert' in error; }; + +export type LyricSearchQuery = { + artist: string; + name: string; +}; diff --git a/src/renderer/features/lyrics/queries/lyric-search-query.ts b/src/renderer/features/lyrics/queries/lyric-search-query.ts new file mode 100644 index 00000000..bff48851 --- /dev/null +++ b/src/renderer/features/lyrics/queries/lyric-search-query.ts @@ -0,0 +1,20 @@ +import { useQuery } from '@tanstack/react-query'; +import isElectron from 'is-electron'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { InternetProviderLyricSearchResponse, LyricSearchQuery } from '/@/renderer/api/types'; +import { QueryHookArgs } from '/@/renderer/lib/react-query'; +import { LyricSource } from '/@/renderer/types'; + +const lyricsIpc = isElectron() ? window.electron.lyrics : null; + +export const useLyricSearch = (args: Omit, 'serverId'>) => { + const { options, query } = args; + + return useQuery>({ + cacheTime: 1000 * 60 * 1, + queryFn: () => lyricsIpc?.searchRemoteLyrics(query), + queryKey: queryKeys.songs.lyricsSearch(query), + staleTime: 1000 * 60 * 1, + ...options, + }); +};