mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 18:13:31 +00:00
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:
parent
23f9bd4e9f
commit
85d2576bdc
25 changed files with 907 additions and 118 deletions
|
|
@ -1,2 +1,3 @@
|
|||
import './lyrics';
|
||||
import './player';
|
||||
import './settings';
|
||||
|
|
|
|||
59
src/main/features/core/lyrics/genius.ts
Normal file
59
src/main/features/core/lyrics/genius.ts
Normal 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;
|
||||
}
|
||||
26
src/main/features/core/lyrics/index.ts
Normal file
26
src/main/features/core/lyrics/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
58
src/main/features/core/lyrics/netease.ts
Normal file
58
src/main/features/core/lyrics/netease.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
17
src/main/preload/lyrics.ts
Normal file
17
src/main/preload/lyrics.ts
Normal 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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue