mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
feat(synced-lyrics): preferred provider (global/per-song) (#3741)
Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
@ -1,5 +1,7 @@
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { providerNames } from './providers';
|
||||
|
||||
import type { MenuItemConstructorOptions } from 'electron';
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
import type { SyncedLyricsPluginConfig } from './types';
|
||||
@ -10,6 +12,35 @@ export const menu = async (
|
||||
const config = await ctx.getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
label: t('plugins.synced-lyrics.menu.preferred-provider.label'),
|
||||
toolTip: t('plugins.synced-lyrics.menu.preferred-provider.tooltip'),
|
||||
type: 'submenu',
|
||||
submenu: [
|
||||
{
|
||||
label: t('plugins.synced-lyrics.menu.preferred-provider.none.label'),
|
||||
toolTip: t(
|
||||
'plugins.synced-lyrics.menu.preferred-provider.none.tooltip',
|
||||
),
|
||||
type: 'radio',
|
||||
checked: config.preferredProvider === undefined,
|
||||
click() {
|
||||
ctx.setConfig({ preferredProvider: undefined });
|
||||
},
|
||||
},
|
||||
...providerNames.map(
|
||||
(provider) =>
|
||||
({
|
||||
label: provider,
|
||||
type: 'radio',
|
||||
checked: config.preferredProvider === provider,
|
||||
click() {
|
||||
ctx.setConfig({ preferredProvider: provider });
|
||||
},
|
||||
}) as const,
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('plugins.synced-lyrics.menu.precise-timing.label'),
|
||||
toolTip: t('plugins.synced-lyrics.menu.precise-timing.tooltip'),
|
||||
|
||||
@ -1,191 +1,21 @@
|
||||
import { createStore } from 'solid-js/store';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { createMemo } from 'solid-js';
|
||||
import type { LyricResult } from '../types';
|
||||
|
||||
import { LRCLib } from './LRCLib';
|
||||
import { LyricsGenius } from './LyricsGenius';
|
||||
import { MusixMatch } from './MusixMatch';
|
||||
import { YTMusic } from './YTMusic';
|
||||
export enum ProviderNames {
|
||||
YTMusic = 'YTMusic',
|
||||
LRCLib = 'LRCLib',
|
||||
MusixMatch = 'MusixMatch',
|
||||
LyricsGenius = 'LyricsGenius',
|
||||
// Megalobiz = 'Megalobiz',
|
||||
}
|
||||
|
||||
import { getSongInfo } from '@/providers/song-info-front';
|
||||
|
||||
import type { LyricProvider, LyricResult } from '../types';
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
|
||||
export const providers = {
|
||||
YTMusic: new YTMusic(),
|
||||
LRCLib: new LRCLib(),
|
||||
MusixMatch: new MusixMatch(),
|
||||
LyricsGenius: new LyricsGenius(),
|
||||
// Megalobiz: new Megalobiz(), // Disabled because it is too unstable and slow
|
||||
} as const;
|
||||
|
||||
export type ProviderName = keyof typeof providers;
|
||||
export const providerNames = Object.keys(providers) as ProviderName[];
|
||||
export const ProviderNameSchema = z.enum(ProviderNames);
|
||||
export type ProviderName = z.infer<typeof ProviderNameSchema>;
|
||||
export const providerNames = ProviderNameSchema.options;
|
||||
|
||||
export type ProviderState = {
|
||||
state: 'fetching' | 'done' | 'error';
|
||||
data: LyricResult | null;
|
||||
error: Error | null;
|
||||
};
|
||||
|
||||
type LyricsStore = {
|
||||
provider: ProviderName;
|
||||
current: ProviderState;
|
||||
lyrics: Record<ProviderName, ProviderState>;
|
||||
};
|
||||
|
||||
const initialData = () =>
|
||||
providerNames.reduce(
|
||||
(acc, name) => {
|
||||
acc[name] = { state: 'fetching', data: null, error: null };
|
||||
return acc;
|
||||
},
|
||||
{} as LyricsStore['lyrics'],
|
||||
);
|
||||
|
||||
export const [lyricsStore, setLyricsStore] = createStore<LyricsStore>({
|
||||
provider: providerNames[0],
|
||||
lyrics: initialData(),
|
||||
get current(): ProviderState {
|
||||
return this.lyrics[this.provider];
|
||||
},
|
||||
});
|
||||
|
||||
export const currentLyrics = createMemo(() => {
|
||||
const provider = lyricsStore.provider;
|
||||
return lyricsStore.lyrics[provider];
|
||||
});
|
||||
|
||||
type VideoId = string;
|
||||
|
||||
type SearchCacheData = Record<ProviderName, ProviderState>;
|
||||
interface SearchCache {
|
||||
state: 'loading' | 'done';
|
||||
data: SearchCacheData;
|
||||
}
|
||||
|
||||
// TODO: Maybe use localStorage for the cache.
|
||||
const searchCache = new Map<VideoId, SearchCache>();
|
||||
export const fetchLyrics = (info: SongInfo) => {
|
||||
if (searchCache.has(info.videoId)) {
|
||||
const cache = searchCache.get(info.videoId)!;
|
||||
|
||||
if (cache.state === 'loading') {
|
||||
setTimeout(() => {
|
||||
fetchLyrics(info);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', () => {
|
||||
// weird bug with solid-js
|
||||
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const cache: SearchCache = {
|
||||
state: 'loading',
|
||||
data: initialData(),
|
||||
};
|
||||
|
||||
searchCache.set(info.videoId, cache);
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', () => {
|
||||
// weird bug with solid-js
|
||||
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
|
||||
});
|
||||
}
|
||||
|
||||
const tasks: Promise<void>[] = [];
|
||||
|
||||
// prettier-ignore
|
||||
for (
|
||||
const [providerName, provider] of Object.entries(providers) as [
|
||||
ProviderName,
|
||||
LyricProvider,
|
||||
][]
|
||||
) {
|
||||
const pCache = cache.data[providerName];
|
||||
|
||||
tasks.push(
|
||||
provider
|
||||
.search(info)
|
||||
.then((res) => {
|
||||
pCache.state = 'done';
|
||||
pCache.data = res;
|
||||
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[providerName]: {
|
||||
state: 'done',
|
||||
data: res ? { ...res } : null,
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
pCache.state = 'error';
|
||||
pCache.error = error;
|
||||
|
||||
console.error(error);
|
||||
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[providerName]: { state: 'error', error, data: null },
|
||||
};
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Promise.allSettled(tasks).then(() => {
|
||||
cache.state = 'done';
|
||||
searchCache.set(info.videoId, cache);
|
||||
});
|
||||
};
|
||||
|
||||
export const retrySearch = (provider: ProviderName, info: SongInfo) => {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
const pCache = {
|
||||
state: 'fetching',
|
||||
data: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
return {
|
||||
...old,
|
||||
[provider]: pCache,
|
||||
};
|
||||
});
|
||||
|
||||
providers[provider]
|
||||
.search(info)
|
||||
.then((res) => {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[provider]: { state: 'done', data: res, error: null },
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[provider]: { state: 'error', data: null, error },
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
13
src/plugins/synced-lyrics/providers/renderer.ts
Normal file
13
src/plugins/synced-lyrics/providers/renderer.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ProviderNames } from './index';
|
||||
import { YTMusic } from './YTMusic';
|
||||
import { LRCLib } from './LRCLib';
|
||||
import { MusixMatch } from './MusixMatch';
|
||||
import { LyricsGenius } from './LyricsGenius';
|
||||
|
||||
export const providers = {
|
||||
[ProviderNames.YTMusic]: new YTMusic(),
|
||||
[ProviderNames.LRCLib]: new LRCLib(),
|
||||
[ProviderNames.MusixMatch]: new MusixMatch(),
|
||||
[ProviderNames.LyricsGenius]: new LyricsGenius(),
|
||||
// [ProviderNames.Megalobiz]: new Megalobiz(), // Disabled because it is too unstable and slow
|
||||
} as const;
|
||||
@ -2,7 +2,7 @@ import { t } from '@/i18n';
|
||||
|
||||
import { getSongInfo } from '@/providers/song-info-front';
|
||||
|
||||
import { lyricsStore, retrySearch } from '../../providers';
|
||||
import { lyricsStore, retrySearch } from '../store';
|
||||
|
||||
interface ErrorDisplayProps {
|
||||
error: Error;
|
||||
|
||||
@ -11,18 +11,24 @@ import {
|
||||
Switch,
|
||||
} from 'solid-js';
|
||||
|
||||
import * as z from 'zod';
|
||||
|
||||
import {
|
||||
currentLyrics,
|
||||
lyricsStore,
|
||||
type ProviderName,
|
||||
providerNames,
|
||||
ProviderNameSchema,
|
||||
type ProviderState,
|
||||
setLyricsStore,
|
||||
} from '../../providers';
|
||||
|
||||
import { currentLyrics, lyricsStore, setLyricsStore } from '../store';
|
||||
import { _ytAPI } from '../index';
|
||||
import { config } from '../renderer';
|
||||
|
||||
import type { YtIcons } from '@/types/icons';
|
||||
import type { PlayerAPIEvents } from '@/types/player-api-events';
|
||||
|
||||
const LocalStorageSchema = z.object({
|
||||
provider: ProviderNameSchema,
|
||||
});
|
||||
|
||||
export const providerIdx = createMemo(() =>
|
||||
providerNames.indexOf(lyricsStore.provider),
|
||||
@ -45,11 +51,19 @@ const providerBias = (p: ProviderName) =>
|
||||
(lyricsStore.lyrics[p].data?.lyrics ? 1 : -1);
|
||||
|
||||
const pickBestProvider = () => {
|
||||
const preferred = config()?.preferredProvider;
|
||||
if (preferred) {
|
||||
const data = lyricsStore.lyrics[preferred].data;
|
||||
if (Array.isArray(data?.lines) || data?.lyrics) {
|
||||
return { provider: preferred, force: true };
|
||||
}
|
||||
}
|
||||
|
||||
const providers = Array.from(providerNames);
|
||||
|
||||
providers.sort((a, b) => providerBias(b) - providerBias(a));
|
||||
|
||||
return providers[0];
|
||||
return { provider: providers[0], force: false };
|
||||
};
|
||||
|
||||
const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
||||
@ -58,34 +72,91 @@ const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
||||
export const LyricsPicker = (props: {
|
||||
setStickRef: Setter<HTMLElement | null>;
|
||||
}) => {
|
||||
const [videoId, setVideoId] = createSignal<string | null>(null);
|
||||
const [starredProvider, setStarredProvider] =
|
||||
createSignal<ProviderName | null>(null);
|
||||
|
||||
createEffect(() => {
|
||||
const id = videoId();
|
||||
if (id === null) {
|
||||
setStarredProvider(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `ytmd-sl-starred-${id}`;
|
||||
const value = localStorage.getItem(key);
|
||||
if (!value) {
|
||||
setStarredProvider(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const parseResult = LocalStorageSchema.safeParse(JSON.parse(value));
|
||||
if (parseResult.success) {
|
||||
setLyricsStore('provider', parseResult.data.provider);
|
||||
setStarredProvider(parseResult.data.provider);
|
||||
} else {
|
||||
setStarredProvider(null);
|
||||
}
|
||||
});
|
||||
|
||||
const toggleStar = () => {
|
||||
const id = videoId();
|
||||
if (id === null) return;
|
||||
|
||||
const key = `ytmd-sl-starred-${id}`;
|
||||
|
||||
setStarredProvider((starredProvider) => {
|
||||
if (lyricsStore.provider === starredProvider) {
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
const provider = lyricsStore.provider;
|
||||
localStorage.setItem(key, JSON.stringify({ provider }));
|
||||
|
||||
return provider;
|
||||
});
|
||||
};
|
||||
|
||||
const videoDataChangeHandler = (
|
||||
name: string,
|
||||
{ videoId }: PlayerAPIEvents['videodatachange']['value'],
|
||||
) => {
|
||||
setVideoId(videoId);
|
||||
|
||||
if (name !== 'dataloaded') return;
|
||||
setHasManuallySwitchedProvider(false);
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
{
|
||||
onMount(() => _ytAPI?.addEventListener('videodatachange', videoDataChangeHandler));
|
||||
onCleanup(() => _ytAPI?.removeEventListener('videodatachange', videoDataChangeHandler));
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
// fallback to the next source, if the current one has an error
|
||||
if (!hasManuallySwitchedProvider()) {
|
||||
const bestProvider = pickBestProvider();
|
||||
const starred = starredProvider();
|
||||
if (starred !== null) {
|
||||
setLyricsStore('provider', starred);
|
||||
return;
|
||||
}
|
||||
|
||||
const allProvidersFailed = providerNames.every((p) =>
|
||||
shouldSwitchProvider(lyricsStore.lyrics[p]),
|
||||
);
|
||||
if (allProvidersFailed) return;
|
||||
|
||||
if (providerBias(lyricsStore.provider) < providerBias(bestProvider)) {
|
||||
setLyricsStore('provider', bestProvider);
|
||||
const { provider, force } = pickBestProvider();
|
||||
if (
|
||||
force ||
|
||||
providerBias(lyricsStore.provider) < providerBias(provider)
|
||||
) {
|
||||
setLyricsStore('provider', provider);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const videoDataChangeHandler = (name: string) => {
|
||||
if (name !== 'dataloaded') return;
|
||||
setHasManuallySwitchedProvider(false);
|
||||
};
|
||||
|
||||
_ytAPI?.addEventListener('videodatachange', videoDataChangeHandler);
|
||||
onCleanup(() =>
|
||||
_ytAPI?.removeEventListener('videodatachange', videoDataChangeHandler),
|
||||
);
|
||||
});
|
||||
|
||||
const next = () => {
|
||||
setHasManuallySwitchedProvider(true);
|
||||
setLyricsStore('provider', (prevProvider) => {
|
||||
@ -176,9 +247,9 @@ export const LyricsPicker = (props: {
|
||||
/>
|
||||
</Match>
|
||||
<Match when={currentLyrics().state === 'error'}>
|
||||
<yt-icon-button
|
||||
<yt-icon
|
||||
icon={errorIcon}
|
||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||
style={{ padding: '5px', transform: 'scale(0.8)' }}
|
||||
tabindex="-1"
|
||||
/>
|
||||
</Match>
|
||||
@ -189,9 +260,9 @@ export const LyricsPicker = (props: {
|
||||
currentLyrics().data?.lyrics)
|
||||
}
|
||||
>
|
||||
<yt-icon-button
|
||||
<yt-icon
|
||||
icon={successIcon}
|
||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||
style={{ padding: '5px', transform: 'scale(0.8)' }}
|
||||
tabindex="-1"
|
||||
/>
|
||||
</Match>
|
||||
@ -202,9 +273,9 @@ export const LyricsPicker = (props: {
|
||||
!currentLyrics().data?.lyrics
|
||||
}
|
||||
>
|
||||
<yt-icon-button
|
||||
<yt-icon
|
||||
icon={notFoundIcon}
|
||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||
style={{ padding: '5px', transform: 'scale(0.8)' }}
|
||||
tabindex="-1"
|
||||
/>
|
||||
</Match>
|
||||
@ -213,6 +284,20 @@ export const LyricsPicker = (props: {
|
||||
class="description ytmusic-description-shelf-renderer"
|
||||
text={{ runs: [{ text: provider() }] }}
|
||||
/>
|
||||
<yt-icon
|
||||
icon={
|
||||
starredProvider() === provider()
|
||||
? 'yt-sys-icons:star-filled'
|
||||
: 'yt-sys-icons:star'
|
||||
}
|
||||
onClick={toggleStar}
|
||||
style={{
|
||||
padding: '5px',
|
||||
transform: 'scale(0.8)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Index>
|
||||
|
||||
@ -3,8 +3,7 @@ import { waitForElement } from '@/utils/wait-for-element';
|
||||
|
||||
import { selectors, tabStates } from './utils';
|
||||
import { setConfig, setCurrentTime } from './renderer';
|
||||
|
||||
import { fetchLyrics } from '../providers';
|
||||
import { fetchLyrics } from './store';
|
||||
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
PlainLyrics,
|
||||
} from './components';
|
||||
|
||||
import { currentLyrics } from '../providers';
|
||||
import { currentLyrics } from './store';
|
||||
|
||||
import type { LineLyrics, SyncedLyricsPluginConfig } from '../types';
|
||||
|
||||
|
||||
175
src/plugins/synced-lyrics/renderer/store.ts
Normal file
175
src/plugins/synced-lyrics/renderer/store.ts
Normal file
@ -0,0 +1,175 @@
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { createMemo } from 'solid-js';
|
||||
|
||||
import { getSongInfo } from '@/providers/song-info-front';
|
||||
|
||||
import {
|
||||
type ProviderName,
|
||||
providerNames,
|
||||
type ProviderState,
|
||||
} from '../providers';
|
||||
import { providers } from '../providers/renderer';
|
||||
|
||||
import type { LyricProvider } from '../types';
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
|
||||
type LyricsStore = {
|
||||
provider: ProviderName;
|
||||
current: ProviderState;
|
||||
lyrics: Record<ProviderName, ProviderState>;
|
||||
};
|
||||
|
||||
const initialData = () =>
|
||||
providerNames.reduce(
|
||||
(acc, name) => {
|
||||
acc[name] = { state: 'fetching', data: null, error: null };
|
||||
return acc;
|
||||
},
|
||||
{} as LyricsStore['lyrics'],
|
||||
);
|
||||
|
||||
export const [lyricsStore, setLyricsStore] = createStore<LyricsStore>({
|
||||
provider: providerNames[0],
|
||||
lyrics: initialData(),
|
||||
get current(): ProviderState {
|
||||
return this.lyrics[this.provider];
|
||||
},
|
||||
});
|
||||
|
||||
export const currentLyrics = createMemo(() => {
|
||||
const provider = lyricsStore.provider;
|
||||
return lyricsStore.lyrics[provider];
|
||||
});
|
||||
|
||||
type VideoId = string;
|
||||
|
||||
type SearchCacheData = Record<ProviderName, ProviderState>;
|
||||
interface SearchCache {
|
||||
state: 'loading' | 'done';
|
||||
data: SearchCacheData;
|
||||
}
|
||||
|
||||
// TODO: Maybe use localStorage for the cache.
|
||||
const searchCache = new Map<VideoId, SearchCache>();
|
||||
export const fetchLyrics = (info: SongInfo) => {
|
||||
if (searchCache.has(info.videoId)) {
|
||||
const cache = searchCache.get(info.videoId)!;
|
||||
|
||||
if (cache.state === 'loading') {
|
||||
setTimeout(() => {
|
||||
fetchLyrics(info);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', () => {
|
||||
// weird bug with solid-js
|
||||
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const cache: SearchCache = {
|
||||
state: 'loading',
|
||||
data: initialData(),
|
||||
};
|
||||
|
||||
searchCache.set(info.videoId, cache);
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', () => {
|
||||
// weird bug with solid-js
|
||||
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
|
||||
});
|
||||
}
|
||||
|
||||
const tasks: Promise<void>[] = [];
|
||||
|
||||
// prettier-ignore
|
||||
for (
|
||||
const [providerName, provider] of Object.entries(providers) as [
|
||||
ProviderName,
|
||||
LyricProvider,
|
||||
][]
|
||||
) {
|
||||
const pCache = cache.data[providerName];
|
||||
|
||||
tasks.push(
|
||||
provider
|
||||
.search(info)
|
||||
.then((res) => {
|
||||
pCache.state = 'done';
|
||||
pCache.data = res;
|
||||
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[providerName]: {
|
||||
state: 'done',
|
||||
data: res ? { ...res } : null,
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
pCache.state = 'error';
|
||||
pCache.error = error;
|
||||
|
||||
console.error(error);
|
||||
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[providerName]: { state: 'error', error, data: null },
|
||||
};
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Promise.allSettled(tasks).then(() => {
|
||||
cache.state = 'done';
|
||||
searchCache.set(info.videoId, cache);
|
||||
});
|
||||
};
|
||||
|
||||
export const retrySearch = (provider: ProviderName, info: SongInfo) => {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
const pCache = {
|
||||
state: 'fetching',
|
||||
data: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
return {
|
||||
...old,
|
||||
[provider]: pCache,
|
||||
};
|
||||
});
|
||||
|
||||
providers[provider]
|
||||
.search(info)
|
||||
.then((res) => {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[provider]: { state: 'done', data: res, error: null },
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[provider]: { state: 'error', data: null, error },
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -1,7 +1,9 @@
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
import type { ProviderName } from './providers';
|
||||
|
||||
export type SyncedLyricsPluginConfig = {
|
||||
enabled: boolean;
|
||||
preferredProvider?: ProviderName;
|
||||
preciseTiming: boolean;
|
||||
showTimeCodes: boolean;
|
||||
defaultTextString: string | string[];
|
||||
|
||||
Reference in New Issue
Block a user