Improved lyric syncing, fetch

- uses a somewhat more sane way to parse lyrics and teardown timeouts
- adds 'seeked' to setCurrentTime to make detecting seeks in lyric much easier
- adds ability to fetch lyrics from genius/netease (desktop only)
This commit is contained in:
Kendall Garner 2023-05-28 14:31:49 -07:00 committed by Jeff
parent 23f9bd4e9f
commit 85d2576bdc
25 changed files with 907 additions and 118 deletions

View file

@ -1,2 +1,3 @@
import './lyrics';
import './player';
import './settings';

View file

@ -0,0 +1,59 @@
import axios, { AxiosResponse } from 'axios';
import { load } from 'cheerio';
import type { QueueSong } from '/@/renderer/api/types';
const search_url = 'https://genius.com/api/search/song';
async function getSongURL(metadata: QueueSong) {
let result: AxiosResponse<any, any>;
try {
result = await axios.get(search_url, {
params: {
per_page: '1',
q: `${metadata.artistName} ${metadata.name}`,
},
});
} catch (e) {
console.error('Genius search request got an error!', e);
return undefined;
}
return result.data.response?.sections?.[0]?.hits?.[0]?.result?.url;
}
async function getLyricsFromGenius(url: string): Promise<string | null> {
let result: AxiosResponse<string, any>;
try {
result = await axios.get<string>(url, { responseType: 'text' });
} catch (e) {
console.error('Genius lyrics request got an error!', e);
return null;
}
const $ = load(result.data.split('<br/>').join('\n'));
const lyricsDiv = $('div.lyrics');
if (lyricsDiv.length > 0) return lyricsDiv.text().trim();
const lyricSections = $('div[class^=Lyrics__Container]')
.map((_, e) => $(e).text())
.toArray()
.join('\n');
return lyricSections;
}
export async function query(metadata: QueueSong): Promise<string | null> {
const songId = await getSongURL(metadata);
if (!songId) {
console.error('Could not find the song on Genius!');
return null;
}
const lyrics = await getLyricsFromGenius(songId);
if (!lyrics) {
console.error('Could not get lyrics on Genius!');
return null;
}
return lyrics;
}

View file

@ -0,0 +1,26 @@
import { QueueSong } from '/@/renderer/api/types';
import { query as queryGenius } from './genius';
import { query as queryNetease } from './netease';
import { LyricSource } from '../../../../renderer/types';
import { ipcMain } from 'electron';
import { getMainWindow } from '../../../main';
import { store } from '../settings/index';
type SongFetcher = (song: QueueSong) => Promise<string | null>;
const FETCHERS: Record<LyricSource, SongFetcher> = {
[LyricSource.GENIUS]: queryGenius,
[LyricSource.NETEASE]: queryNetease,
};
ipcMain.on('lyric-fetch', async (_event, song: QueueSong) => {
const sources = store.get('lyrics', []) as LyricSource[];
for (const source of sources) {
const lyric = await FETCHERS[source](song);
if (lyric) {
getMainWindow()?.webContents.send('lyric-get', song.name, source, lyric);
break;
}
}
});

View file

@ -0,0 +1,58 @@
import axios, { AxiosResponse } from 'axios';
import type { 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';
async function getSongId(metadata: QueueSong) {
let result: AxiosResponse<any, any>;
try {
result = await axios.get(SEARCH_URL, {
params: {
limit: 10,
offset: 0,
s: `${metadata.artistName} ${metadata.name}`,
type: '1',
},
});
} catch (e) {
console.error('NetEase search request got an error!', e);
return undefined;
}
return result?.data.result?.songs?.[0].id;
}
async function getLyricsFromSongId(songId: string) {
let result: AxiosResponse<any, any>;
try {
result = await axios.get(LYRICS_URL, {
params: {
id: songId,
kv: '-1',
lv: '-1',
},
});
} catch (e) {
console.error('NetEase lyrics request got an error!', e);
return undefined;
}
return result.data.klyric?.lyric || result.data.lrc?.lyric;
}
export async function query(metadata: QueueSong): Promise<string | null> {
const songId = await getSongId(metadata);
if (!songId) {
console.error('Could not find the song on NetEase!');
return null;
}
const lyrics = await getLyricsFromSongId(songId);
if (!lyrics) {
console.error('Could not get lyrics on NetEase!');
return null;
}
return lyrics;
}

View file

@ -128,3 +128,7 @@ ipcMain.on('player-volume', async (_event, value: number) => {
ipcMain.on('player-mute', async () => {
await getMpvInstance()?.mute();
});
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
return getMpvInstance()?.getTimePosition();
});

View file

@ -2,6 +2,7 @@ import { contextBridge } from 'electron';
import { browser } from './preload/browser';
import { ipc } from './preload/ipc';
import { localSettings } from './preload/local-settings';
import { lyrics } from './preload/lyrics';
import { mpris } from './preload/mpris';
import { mpvPlayer, mpvPlayerListener } from './preload/mpv-player';
import { utils } from './preload/utils';
@ -10,6 +11,7 @@ contextBridge.exposeInMainWorld('electron', {
browser,
ipc,
localSettings,
lyrics,
mpris,
mpvPlayer,
mpvPlayerListener,

View file

@ -0,0 +1,17 @@
import { IpcRendererEvent, ipcRenderer } from 'electron';
import { QueueSong } from '/@/renderer/api/types';
const fetchLyrics = (song: QueueSong) => {
ipcRenderer.send('lyric-fetch', song);
};
const getLyrics = (
cb: (event: IpcRendererEvent, songName: string, source: string, lyric: string) => void,
) => {
ipcRenderer.on('lyric-get', cb);
};
export const lyrics = {
fetchLyrics,
getLyrics,
};

View file

@ -78,6 +78,10 @@ const quit = () => {
ipcRenderer.send('player-quit');
};
const getCurrentTime = async () => {
return ipcRenderer.invoke('player-get-time');
};
const rendererAutoNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-auto-next', cb);
};
@ -157,6 +161,7 @@ const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
export const mpvPlayer = {
autoNext,
currentTime,
getCurrentTime,
initialize,
mute,
next,