Skip to content

Commit be3ae4d

Browse files
feat(synced-lyrics): preferred provider (global/per-song) (#3741)
Co-authored-by: JellyBrick <[email protected]>
1 parent 336b7fe commit be3ae4d

File tree

11 files changed

+358
-215
lines changed

11 files changed

+358
-215
lines changed

src/i18n/resources/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,14 @@
813813
"not-found": "⚠️ No lyrics found for this song."
814814
},
815815
"menu": {
816+
"preferred-provider": {
817+
"label": "Preferred Provider",
818+
"tooltip": "Choose the default provider to use",
819+
"none": {
820+
"label": "None",
821+
"tooltip": "No preferred provider"
822+
}
823+
},
816824
"default-text-string": {
817825
"label": "Default character between lyrics",
818826
"tooltip": "Choose the default character to use for the gap between lyrics"

src/plugins/synced-lyrics/menu.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { t } from '@/i18n';
22

3+
import { providerNames } from './providers';
4+
35
import type { MenuItemConstructorOptions } from 'electron';
46
import type { MenuContext } from '@/types/contexts';
57
import type { SyncedLyricsPluginConfig } from './types';
@@ -10,6 +12,35 @@ export const menu = async (
1012
const config = await ctx.getConfig();
1113

1214
return [
15+
{
16+
label: t('plugins.synced-lyrics.menu.preferred-provider.label'),
17+
toolTip: t('plugins.synced-lyrics.menu.preferred-provider.tooltip'),
18+
type: 'submenu',
19+
submenu: [
20+
{
21+
label: t('plugins.synced-lyrics.menu.preferred-provider.none.label'),
22+
toolTip: t(
23+
'plugins.synced-lyrics.menu.preferred-provider.none.tooltip',
24+
),
25+
type: 'radio',
26+
checked: config.preferredProvider === undefined,
27+
click() {
28+
ctx.setConfig({ preferredProvider: undefined });
29+
},
30+
},
31+
...providerNames.map(
32+
(provider) =>
33+
({
34+
label: provider,
35+
type: 'radio',
36+
checked: config.preferredProvider === provider,
37+
click() {
38+
ctx.setConfig({ preferredProvider: provider });
39+
},
40+
}) as const,
41+
),
42+
],
43+
},
1344
{
1445
label: t('plugins.synced-lyrics.menu.precise-timing.label'),
1546
toolTip: t('plugins.synced-lyrics.menu.precise-timing.tooltip'),
Lines changed: 12 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -1,191 +1,21 @@
1-
import { createStore } from 'solid-js/store';
1+
import * as z from 'zod';
22

3-
import { createMemo } from 'solid-js';
3+
import type { LyricResult } from '../types';
44

5-
import { LRCLib } from './LRCLib';
6-
import { LyricsGenius } from './LyricsGenius';
7-
import { MusixMatch } from './MusixMatch';
8-
import { YTMusic } from './YTMusic';
9-
10-
import { getSongInfo } from '@/providers/song-info-front';
11-
12-
import type { LyricProvider, LyricResult } from '../types';
13-
import type { SongInfo } from '@/providers/song-info';
14-
15-
export const providers = {
16-
YTMusic: new YTMusic(),
17-
LRCLib: new LRCLib(),
18-
MusixMatch: new MusixMatch(),
19-
LyricsGenius: new LyricsGenius(),
20-
// Megalobiz: new Megalobiz(), // Disabled because it is too unstable and slow
21-
} as const;
5+
export enum ProviderNames {
6+
YTMusic = 'YTMusic',
7+
LRCLib = 'LRCLib',
8+
MusixMatch = 'MusixMatch',
9+
LyricsGenius = 'LyricsGenius',
10+
// Megalobiz = 'Megalobiz',
11+
}
2212

23-
export type ProviderName = keyof typeof providers;
24-
export const providerNames = Object.keys(providers) as ProviderName[];
13+
export const ProviderNameSchema = z.enum(ProviderNames);
14+
export type ProviderName = z.infer<typeof ProviderNameSchema>;
15+
export const providerNames = ProviderNameSchema.options;
2516

2617
export type ProviderState = {
2718
state: 'fetching' | 'done' | 'error';
2819
data: LyricResult | null;
2920
error: Error | null;
3021
};
31-
32-
type LyricsStore = {
33-
provider: ProviderName;
34-
current: ProviderState;
35-
lyrics: Record<ProviderName, ProviderState>;
36-
};
37-
38-
const initialData = () =>
39-
providerNames.reduce(
40-
(acc, name) => {
41-
acc[name] = { state: 'fetching', data: null, error: null };
42-
return acc;
43-
},
44-
{} as LyricsStore['lyrics'],
45-
);
46-
47-
export const [lyricsStore, setLyricsStore] = createStore<LyricsStore>({
48-
provider: providerNames[0],
49-
lyrics: initialData(),
50-
get current(): ProviderState {
51-
return this.lyrics[this.provider];
52-
},
53-
});
54-
55-
export const currentLyrics = createMemo(() => {
56-
const provider = lyricsStore.provider;
57-
return lyricsStore.lyrics[provider];
58-
});
59-
60-
type VideoId = string;
61-
62-
type SearchCacheData = Record<ProviderName, ProviderState>;
63-
interface SearchCache {
64-
state: 'loading' | 'done';
65-
data: SearchCacheData;
66-
}
67-
68-
// TODO: Maybe use localStorage for the cache.
69-
const searchCache = new Map<VideoId, SearchCache>();
70-
export const fetchLyrics = (info: SongInfo) => {
71-
if (searchCache.has(info.videoId)) {
72-
const cache = searchCache.get(info.videoId)!;
73-
74-
if (cache.state === 'loading') {
75-
setTimeout(() => {
76-
fetchLyrics(info);
77-
});
78-
return;
79-
}
80-
81-
if (getSongInfo().videoId === info.videoId) {
82-
setLyricsStore('lyrics', () => {
83-
// weird bug with solid-js
84-
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
85-
});
86-
}
87-
88-
return;
89-
}
90-
91-
const cache: SearchCache = {
92-
state: 'loading',
93-
data: initialData(),
94-
};
95-
96-
searchCache.set(info.videoId, cache);
97-
if (getSongInfo().videoId === info.videoId) {
98-
setLyricsStore('lyrics', () => {
99-
// weird bug with solid-js
100-
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
101-
});
102-
}
103-
104-
const tasks: Promise<void>[] = [];
105-
106-
// prettier-ignore
107-
for (
108-
const [providerName, provider] of Object.entries(providers) as [
109-
ProviderName,
110-
LyricProvider,
111-
][]
112-
) {
113-
const pCache = cache.data[providerName];
114-
115-
tasks.push(
116-
provider
117-
.search(info)
118-
.then((res) => {
119-
pCache.state = 'done';
120-
pCache.data = res;
121-
122-
if (getSongInfo().videoId === info.videoId) {
123-
setLyricsStore('lyrics', (old) => {
124-
return {
125-
...old,
126-
[providerName]: {
127-
state: 'done',
128-
data: res ? { ...res } : null,
129-
error: null,
130-
},
131-
};
132-
});
133-
}
134-
})
135-
.catch((error: Error) => {
136-
pCache.state = 'error';
137-
pCache.error = error;
138-
139-
console.error(error);
140-
141-
if (getSongInfo().videoId === info.videoId) {
142-
setLyricsStore('lyrics', (old) => {
143-
return {
144-
...old,
145-
[providerName]: { state: 'error', error, data: null },
146-
};
147-
});
148-
}
149-
}),
150-
);
151-
}
152-
153-
Promise.allSettled(tasks).then(() => {
154-
cache.state = 'done';
155-
searchCache.set(info.videoId, cache);
156-
});
157-
};
158-
159-
export const retrySearch = (provider: ProviderName, info: SongInfo) => {
160-
setLyricsStore('lyrics', (old) => {
161-
const pCache = {
162-
state: 'fetching',
163-
data: null,
164-
error: null,
165-
};
166-
167-
return {
168-
...old,
169-
[provider]: pCache,
170-
};
171-
});
172-
173-
providers[provider]
174-
.search(info)
175-
.then((res) => {
176-
setLyricsStore('lyrics', (old) => {
177-
return {
178-
...old,
179-
[provider]: { state: 'done', data: res, error: null },
180-
};
181-
});
182-
})
183-
.catch((error) => {
184-
setLyricsStore('lyrics', (old) => {
185-
return {
186-
...old,
187-
[provider]: { state: 'error', data: null, error },
188-
};
189-
});
190-
});
191-
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ProviderNames } from './index';
2+
import { YTMusic } from './YTMusic';
3+
import { LRCLib } from './LRCLib';
4+
import { MusixMatch } from './MusixMatch';
5+
import { LyricsGenius } from './LyricsGenius';
6+
7+
export const providers = {
8+
[ProviderNames.YTMusic]: new YTMusic(),
9+
[ProviderNames.LRCLib]: new LRCLib(),
10+
[ProviderNames.MusixMatch]: new MusixMatch(),
11+
[ProviderNames.LyricsGenius]: new LyricsGenius(),
12+
// [ProviderNames.Megalobiz]: new Megalobiz(), // Disabled because it is too unstable and slow
13+
} as const;

src/plugins/synced-lyrics/renderer/components/ErrorDisplay.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { t } from '@/i18n';
22

33
import { getSongInfo } from '@/providers/song-info-front';
44

5-
import { lyricsStore, retrySearch } from '../../providers';
5+
import { lyricsStore, retrySearch } from '../store';
66

77
interface ErrorDisplayProps {
88
error: Error;

0 commit comments

Comments
 (0)