Add LrcLib Fetcher (#136)
* lrclib, do not show search/clear buttons if no fetchers configured
This commit is contained in:
parent
d7ca25525c
commit
d6e628099c
5 changed files with 141 additions and 5 deletions
|
@ -121,7 +121,7 @@ export async function getSearchResults(
|
||||||
|
|
||||||
if (!rawSongsResult) return null;
|
if (!rawSongsResult) return null;
|
||||||
|
|
||||||
const songResults: InternetProviderLyricSearchResponse[] = rawSongsResult.map((song: any) => {
|
const songResults: InternetProviderLyricSearchResponse[] = rawSongsResult.map((song) => {
|
||||||
return {
|
return {
|
||||||
artist: song.artist_names,
|
artist: song.artist_names,
|
||||||
id: song.url,
|
id: song.url,
|
||||||
|
|
|
@ -13,6 +13,11 @@ import {
|
||||||
getSearchResults as searchGenius,
|
getSearchResults as searchGenius,
|
||||||
getLyricsBySongId as getGenius,
|
getLyricsBySongId as getGenius,
|
||||||
} from './genius';
|
} from './genius';
|
||||||
|
import {
|
||||||
|
query as queryLrclib,
|
||||||
|
getSearchResults as searchLrcLib,
|
||||||
|
getLyricsBySongId as getLrcLib,
|
||||||
|
} from './lrclib';
|
||||||
import {
|
import {
|
||||||
query as queryNetease,
|
query as queryNetease,
|
||||||
getSearchResults as searchNetease,
|
getSearchResults as searchNetease,
|
||||||
|
@ -29,16 +34,19 @@ type CachedLyrics = Record<LyricSource, InternetProviderLyricResponse>;
|
||||||
|
|
||||||
const FETCHERS: Record<LyricSource, SongFetcher> = {
|
const FETCHERS: Record<LyricSource, SongFetcher> = {
|
||||||
[LyricSource.GENIUS]: queryGenius,
|
[LyricSource.GENIUS]: queryGenius,
|
||||||
|
[LyricSource.LRCLIB]: queryLrclib,
|
||||||
[LyricSource.NETEASE]: queryNetease,
|
[LyricSource.NETEASE]: queryNetease,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SEARCH_FETCHERS: Record<LyricSource, SearchFetcher> = {
|
const SEARCH_FETCHERS: Record<LyricSource, SearchFetcher> = {
|
||||||
[LyricSource.GENIUS]: searchGenius,
|
[LyricSource.GENIUS]: searchGenius,
|
||||||
|
[LyricSource.LRCLIB]: searchLrcLib,
|
||||||
[LyricSource.NETEASE]: searchNetease,
|
[LyricSource.NETEASE]: searchNetease,
|
||||||
};
|
};
|
||||||
|
|
||||||
const GET_FETCHERS: Record<LyricSource, GetFetcher> = {
|
const GET_FETCHERS: Record<LyricSource, GetFetcher> = {
|
||||||
[LyricSource.GENIUS]: getGenius,
|
[LyricSource.GENIUS]: getGenius,
|
||||||
|
[LyricSource.LRCLIB]: getLrcLib,
|
||||||
[LyricSource.NETEASE]: getNetease,
|
[LyricSource.NETEASE]: getNetease,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -61,7 +69,12 @@ const getRemoteLyrics = async (song: QueueSong) => {
|
||||||
let lyricsFromSource = null;
|
let lyricsFromSource = null;
|
||||||
|
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
const params = { artist: song.artistName, name: song.name };
|
const params = {
|
||||||
|
album: song.album || song.name,
|
||||||
|
artist: song.artistName,
|
||||||
|
duration: song.duration,
|
||||||
|
name: song.name,
|
||||||
|
};
|
||||||
const response = await FETCHERS[source](params);
|
const response = await FETCHERS[source](params);
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
|
@ -92,6 +105,7 @@ const searchRemoteLyrics = async (params: LyricSearchQuery) => {
|
||||||
|
|
||||||
const results: Record<LyricSource, InternetProviderLyricSearchResponse[]> = {
|
const results: Record<LyricSource, InternetProviderLyricSearchResponse[]> = {
|
||||||
[LyricSource.GENIUS]: [],
|
[LyricSource.GENIUS]: [],
|
||||||
|
[LyricSource.LRCLIB]: [],
|
||||||
[LyricSource.NETEASE]: [],
|
[LyricSource.NETEASE]: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
119
src/main/features/core/lyrics/lrclib.ts
Normal file
119
src/main/features/core/lyrics/lrclib.ts
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// Credits to https://github.com/tranxuanthang/lrcget for API implementation
|
||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import {
|
||||||
|
InternetProviderLyricResponse,
|
||||||
|
InternetProviderLyricSearchResponse,
|
||||||
|
LyricSearchQuery,
|
||||||
|
LyricSource,
|
||||||
|
} from '../../../../renderer/api/types';
|
||||||
|
import { orderSearchResults } from './shared';
|
||||||
|
|
||||||
|
const FETCH_URL = 'https://lrclib.net/api/get';
|
||||||
|
const SEEARCH_URL = 'https://lrclib.net/api/search';
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
export interface LrcLibSearchResponse {
|
||||||
|
albumName: string;
|
||||||
|
artistName: string;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LrcLibTrackResponse {
|
||||||
|
albumName: string;
|
||||||
|
artistName: string;
|
||||||
|
duration: number;
|
||||||
|
id: number;
|
||||||
|
instrumental: boolean;
|
||||||
|
isrc: string;
|
||||||
|
lang: string;
|
||||||
|
name: string;
|
||||||
|
plainLyrics: string | null;
|
||||||
|
releaseDate: string;
|
||||||
|
spotifyId: string;
|
||||||
|
syncedLyrics: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSearchResults(
|
||||||
|
params: LyricSearchQuery,
|
||||||
|
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
||||||
|
let result: AxiosResponse<LrcLibSearchResponse[]>;
|
||||||
|
|
||||||
|
if (!params.name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await axios.get<LrcLibSearchResponse[]>(SEEARCH_URL, {
|
||||||
|
params: {
|
||||||
|
q: params.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('LrcLib search request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.data) return null;
|
||||||
|
|
||||||
|
const songResults: InternetProviderLyricSearchResponse[] = result.data.map((song) => {
|
||||||
|
return {
|
||||||
|
artist: song.artistName,
|
||||||
|
id: String(song.id),
|
||||||
|
name: song.name,
|
||||||
|
source: LyricSource.LRCLIB,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return orderSearchResults({ params, results: songResults });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLyricsBySongId(songId: string): Promise<string | null> {
|
||||||
|
let result: AxiosResponse<LrcLibTrackResponse, any>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('LrcLib lyrics request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data.syncedLyrics || result.data.plainLyrics || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function query(
|
||||||
|
params: LyricSearchQuery,
|
||||||
|
): Promise<InternetProviderLyricResponse | null> {
|
||||||
|
let result: AxiosResponse<LrcLibTrackResponse, any>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await axios.get<LrcLibTrackResponse>(FETCH_URL, {
|
||||||
|
params: {
|
||||||
|
album_name: params.album,
|
||||||
|
artist_name: params.artist,
|
||||||
|
duration: params.duration,
|
||||||
|
track_name: params.name,
|
||||||
|
},
|
||||||
|
timeout: TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('LrcLib search request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lyrics = result.data.syncedLyrics || result.data.plainLyrics || null;
|
||||||
|
|
||||||
|
if (!lyrics) {
|
||||||
|
console.error(`Could not get lyrics on LrcLib!`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
artist: result.data.artistName,
|
||||||
|
id: String(result.data.id),
|
||||||
|
lyrics,
|
||||||
|
name: result.data.name,
|
||||||
|
source: LyricSource.LRCLIB,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1064,7 +1064,9 @@ export const instanceOfCancellationError = (error: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LyricSearchQuery = {
|
export type LyricSearchQuery = {
|
||||||
|
album?: string;
|
||||||
artist?: string;
|
artist?: string;
|
||||||
|
duration?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1075,6 +1077,7 @@ export type LyricGetQuery = {
|
||||||
|
|
||||||
export enum LyricSource {
|
export enum LyricSource {
|
||||||
GENIUS = 'Genius',
|
GENIUS = 'Genius',
|
||||||
|
LRCLIB = 'lrclib.net',
|
||||||
NETEASE = 'NetEase',
|
NETEASE = 'NetEase',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ interface LyricsActionsProps {
|
||||||
export const LyricsActions = ({ onRemoveLyric, onSearchOverride }: LyricsActionsProps) => {
|
export const LyricsActions = ({ onRemoveLyric, onSearchOverride }: LyricsActionsProps) => {
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
const { delayMs } = useLyricsSettings();
|
const { delayMs, sources } = useLyricsSettings();
|
||||||
|
|
||||||
const handleLyricOffset = (e: number) => {
|
const handleLyricOffset = (e: number) => {
|
||||||
setSettings({
|
setSettings({
|
||||||
|
@ -34,7 +34,7 @@ export const LyricsActions = ({ onRemoveLyric, onSearchOverride }: LyricsActions
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDesktop ? (
|
{isDesktop && sources.length ? (
|
||||||
<Button
|
<Button
|
||||||
uppercase
|
uppercase
|
||||||
disabled={isActionsDisabled}
|
disabled={isActionsDisabled}
|
||||||
|
@ -76,7 +76,7 @@ export const LyricsActions = ({ onRemoveLyric, onSearchOverride }: LyricsActions
|
||||||
>
|
>
|
||||||
<RiAddFill />
|
<RiAddFill />
|
||||||
</Button>
|
</Button>
|
||||||
{isDesktop ? (
|
{isDesktop && sources.length ? (
|
||||||
<Button
|
<Button
|
||||||
uppercase
|
uppercase
|
||||||
disabled={isActionsDisabled}
|
disabled={isActionsDisabled}
|
||||||
|
|
Reference in a new issue