From 007a099951802421eb784f99aac5cc73659cac2b Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sun, 4 Jun 2023 23:15:36 -0700 Subject: [PATCH] Lyrics Improvements - Make the settings text actually consistent with behavior - Add metadata (artist/track name) for fetched tracks - Add ability to remove incorrectly fetched lyric - Add lyric fetch cache; save the last 10 fetches - Add ability to change offset in full screen, add more comments --- src/main/features/core/lyrics/genius.ts | 36 ++++-- src/main/features/core/lyrics/index.ts | 37 +++++- src/main/features/core/lyrics/netease.ts | 38 ++++-- src/main/preload/lyrics.ts | 9 +- src/renderer/api/types.ts | 8 ++ .../virtual-table/table-config-dropdown.tsx | 20 +++ src/renderer/features/lyrics/lyric-skip.tsx | 32 +++++ src/renderer/features/lyrics/lyrics.tsx | 44 +++++-- .../features/lyrics/synchronized-lyrics.tsx | 122 ++++++++++++++---- .../features/lyrics/unsynchronized-lyrics.tsx | 20 ++- src/renderer/preload.d.ts | 9 +- 11 files changed, 314 insertions(+), 61 deletions(-) create mode 100644 src/renderer/features/lyrics/lyric-skip.tsx diff --git a/src/main/features/core/lyrics/genius.ts b/src/main/features/core/lyrics/genius.ts index 4954a960..c5851d05 100644 --- a/src/main/features/core/lyrics/genius.ts +++ b/src/main/features/core/lyrics/genius.ts @@ -1,12 +1,18 @@ import axios, { AxiosResponse } from 'axios'; import { load } from 'cheerio'; -import type { QueueSong } from '/@/renderer/api/types'; +import type { InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types'; const SEARCH_URL = 'https://genius.com/api/search/song'; // Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/genius.ts -async function getSongURL(metadata: QueueSong) { +interface GeniusResponse { + artist: string; + title: string; + url: string; +} + +async function getSongURL(metadata: QueueSong): Promise { let result: AxiosResponse; try { result = await axios.get(SEARCH_URL, { @@ -20,7 +26,17 @@ async function getSongURL(metadata: QueueSong) { return undefined; } - return result.data.response?.sections?.[0]?.hits?.[0]?.result?.url; + const hit = result.data.response?.sections?.[0]?.hits?.[0]?.result; + + if (!hit) { + return undefined; + } + + return { + artist: hit.artist_names, + title: hit.full_title, + url: hit.url, + }; } async function getLyricsFromGenius(url: string): Promise { @@ -44,18 +60,22 @@ async function getLyricsFromGenius(url: string): Promise { return lyricSections; } -export async function query(metadata: QueueSong): Promise { - const songId = await getSongURL(metadata); - if (!songId) { +export async function query(metadata: QueueSong): Promise { + const response = await getSongURL(metadata); + if (!response) { console.error('Could not find the song on Genius!'); return null; } - const lyrics = await getLyricsFromGenius(songId); + const lyrics = await getLyricsFromGenius(response.url); if (!lyrics) { console.error('Could not get lyrics on Genius!'); return null; } - return lyrics; + return { + artist: response.artist, + lyrics, + title: response.title, + }; } diff --git a/src/main/features/core/lyrics/index.ts b/src/main/features/core/lyrics/index.ts index 02609bdc..644e4a29 100644 --- a/src/main/features/core/lyrics/index.ts +++ b/src/main/features/core/lyrics/index.ts @@ -1,4 +1,4 @@ -import { QueueSong } from '/@/renderer/api/types'; +import { InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types'; import { query as queryGenius } from './genius'; import { query as queryNetease } from './netease'; import { LyricSource } from '../../../../renderer/types'; @@ -6,19 +6,52 @@ import { ipcMain } from 'electron'; import { getMainWindow } from '../../../main'; import { store } from '../settings/index'; -type SongFetcher = (song: QueueSong) => Promise; +type SongFetcher = (song: QueueSong) => Promise; + +type CachedLyrics = Record; const FETCHERS: Record = { [LyricSource.GENIUS]: queryGenius, [LyricSource.NETEASE]: queryNetease, }; +const MAX_CACHED_ITEMS = 10; + +const lyricCache = new Map(); + ipcMain.on('lyric-fetch', async (_event, song: QueueSong) => { const sources = store.get('lyrics', []) as LyricSource[]; + const cached = lyricCache.get(song.id); + + if (cached) { + for (const source of sources) { + const data = cached[source]; + + if (data) { + getMainWindow()?.webContents.send('lyric-get', song.name, source, data); + return; + } + } + } + for (const source of sources) { const lyric = await FETCHERS[source](song); if (lyric) { + const newResult = cached + ? { + ...cached, + [source]: lyric, + } + : ({ [source]: lyric } as CachedLyrics); + + if (lyricCache.size === MAX_CACHED_ITEMS && cached === undefined) { + const toRemove = lyricCache.keys().next().value; + lyricCache.delete(toRemove); + } + + lyricCache.set(song.id, newResult); + getMainWindow()?.webContents.send('lyric-get', song.name, source, lyric); break; } diff --git a/src/main/features/core/lyrics/netease.ts b/src/main/features/core/lyrics/netease.ts index 5232ce1b..231cf1dc 100644 --- a/src/main/features/core/lyrics/netease.ts +++ b/src/main/features/core/lyrics/netease.ts @@ -1,12 +1,18 @@ import axios, { AxiosResponse } from 'axios'; -import type { QueueSong } from '/@/renderer/api/types'; +import type { InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types'; const SEARCH_URL = 'https://music.163.com/api/search/get'; const LYRICS_URL = 'https://music.163.com/api/song/lyric'; // Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/netease.ts -async function getSongId(metadata: QueueSong) { +interface NetEaseResponse { + artist: string; + id: string; + title: string; +} + +async function getSongId(metadata: QueueSong): Promise { let result: AxiosResponse; try { result = await axios.get(SEARCH_URL, { @@ -22,10 +28,20 @@ async function getSongId(metadata: QueueSong) { return undefined; } - return result?.data.result?.songs?.[0].id; + const song = result?.data.result?.songs?.[0]; + + if (!song) return undefined; + + const artist = song.artists ? song.artists.map((artist: any) => artist.name).join(', ') : ''; + + return { + artist, + id: song.id, + title: song.name, + }; } -async function getLyricsFromSongId(songId: string) { +async function getLyricsFromSongId(songId: string): Promise { let result: AxiosResponse; try { result = await axios.get(LYRICS_URL, { @@ -43,18 +59,22 @@ async function getLyricsFromSongId(songId: string) { return result.data.klyric?.lyric || result.data.lrc?.lyric; } -export async function query(metadata: QueueSong): Promise { - const songId = await getSongId(metadata); - if (!songId) { +export async function query(metadata: QueueSong): Promise { + const response = await getSongId(metadata); + if (!response) { console.error('Could not find the song on NetEase!'); return null; } - const lyrics = await getLyricsFromSongId(songId); + const lyrics = await getLyricsFromSongId(response.id); if (!lyrics) { console.error('Could not get lyrics on NetEase!'); return null; } - return lyrics; + return { + artist: response.artist, + lyrics, + title: response.title, + }; } diff --git a/src/main/preload/lyrics.ts b/src/main/preload/lyrics.ts index 7af46974..c21473b4 100644 --- a/src/main/preload/lyrics.ts +++ b/src/main/preload/lyrics.ts @@ -1,12 +1,17 @@ import { IpcRendererEvent, ipcRenderer } from 'electron'; -import { QueueSong } from '/@/renderer/api/types'; +import { InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types'; const fetchRemoteLyrics = (song: QueueSong) => { ipcRenderer.send('lyric-fetch', song); }; const remoteLyricsListener = ( - cb: (event: IpcRendererEvent, songName: string, source: string, lyric: string) => void, + cb: ( + event: IpcRendererEvent, + songName: string, + source: string, + lyric: InternetProviderLyricResponse, + ) => void, ) => { ipcRenderer.on('lyric-get', cb); }; diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 8f610b09..18ba6b3e 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -1029,6 +1029,14 @@ export type SynchronizedLyricsArray = Array<[number, string]>; export type LyricsResponse = SynchronizedLyricsArray | string; +export type InternetProviderLyricResponse = { + artist: string; + lyrics: string; + title: string; +}; + +export type LyricOverride = Omit; + export const instanceOfCancellationError = (error: any) => { return 'revert' in error; }; diff --git a/src/renderer/components/virtual-table/table-config-dropdown.tsx b/src/renderer/components/virtual-table/table-config-dropdown.tsx index 77954ad3..45c4f03d 100644 --- a/src/renderer/components/virtual-table/table-config-dropdown.tsx +++ b/src/renderer/components/virtual-table/table-config-dropdown.tsx @@ -9,6 +9,7 @@ import { } from '/@/renderer/store/settings.store'; import { TableColumn, TableType } from '/@/renderer/types'; import { Option } from '/@/renderer/components/option'; +import { NumberInput } from '/@/renderer/components/input'; export const SONG_TABLE_COLUMNS = [ { label: 'Row Index', value: TableColumn.ROW_INDEX }, @@ -180,6 +181,15 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => { }); }; + const handleLyricOffset = (e: ChangeEvent) => { + setSettings({ + lyrics: { + ...useSettingsStore.getState().lyrics, + delayMs: Number(e.currentTarget.value), + }, + }); + }; + return ( <> +