mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-17 13:12:07 +00:00
Compare commits
8 Commits
5ecd39f324
...
v3.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 721271d902 | |||
| a2151930ec | |||
| bdc9f42681 | |||
| 0b3c6f9e1f | |||
| ebe373bdc6 | |||
| ab91e6d735 | |||
| be3ae4d789 | |||
| 336b7fe5e9 |
@ -45,7 +45,7 @@ export default defineConfig({
|
|||||||
formats: ['es'],
|
formats: ['es'],
|
||||||
},
|
},
|
||||||
outDir: 'dist/main',
|
outDir: 'dist/main',
|
||||||
rollupOptions: {
|
rolldownOptions: {
|
||||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||||
input: './src/index.ts',
|
input: './src/index.ts',
|
||||||
},
|
},
|
||||||
@ -96,7 +96,7 @@ export default defineConfig({
|
|||||||
commonjsOptions: {
|
commonjsOptions: {
|
||||||
ignoreDynamicRequires: true,
|
ignoreDynamicRequires: true,
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rolldownOptions: {
|
||||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||||
input: './src/preload.ts',
|
input: './src/preload.ts',
|
||||||
},
|
},
|
||||||
@ -149,7 +149,7 @@ export default defineConfig({
|
|||||||
name: 'renderer',
|
name: 'renderer',
|
||||||
},
|
},
|
||||||
outDir: 'dist/renderer',
|
outDir: 'dist/renderer',
|
||||||
rollupOptions: {
|
rolldownOptions: {
|
||||||
external: ['electron', ...builtinModules],
|
external: ['electron', ...builtinModules],
|
||||||
input: './src/index.html',
|
input: './src/index.html',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"desktopName": "com.github.th_ch.youtube_music",
|
"desktopName": "com.github.th_ch.youtube_music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "3.10.0",
|
"version": "3.11.0",
|
||||||
"description": "YouTube Music Desktop App - including custom plugins",
|
"description": "YouTube Music Desktop App - including custom plugins",
|
||||||
"main": "./dist/main/index.js",
|
"main": "./dist/main/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -20,8 +20,8 @@
|
|||||||
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
|
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
|
||||||
"start": "pnpm electron-vite preview",
|
"start": "pnpm electron-vite preview",
|
||||||
"start:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
"start:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
||||||
"dev": "pnpm cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
|
"dev": "pnpm cross-env NODE_ENV=development NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
|
||||||
"dev:renderer": "pnpm cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev",
|
"dev:renderer": "pnpm cross-env NODE_ENV=development NODE_OPTIONS=--enable-source-maps electron-vite dev",
|
||||||
"dev:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
"dev:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
||||||
"clean": "pnpm del-cli dist && pnpm del-cli pack && pnpm del-cli .vite-inspect",
|
"clean": "pnpm del-cli dist && pnpm del-cli pack && pnpm del-cli .vite-inspect",
|
||||||
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
|
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
|
||||||
@ -33,7 +33,7 @@
|
|||||||
"dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never",
|
"dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never",
|
||||||
"dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
|
"dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
|
||||||
"lint": "pnpm eslint ./src",
|
"lint": "pnpm eslint ./src",
|
||||||
"changelog": "pnpm dlx --yes auto-changelog",
|
"changelog": "pnpm dlx auto-changelog",
|
||||||
"release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github",
|
"release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github",
|
||||||
"release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always",
|
"release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always",
|
||||||
"release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always",
|
"release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always",
|
||||||
|
|||||||
@ -813,6 +813,14 @@
|
|||||||
"not-found": "⚠️ No lyrics found for this song."
|
"not-found": "⚠️ No lyrics found for this song."
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
|
"preferred-provider": {
|
||||||
|
"label": "Preferred Provider",
|
||||||
|
"tooltip": "Choose the default provider to use",
|
||||||
|
"none": {
|
||||||
|
"label": "None",
|
||||||
|
"tooltip": "No preferred provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
"default-text-string": {
|
"default-text-string": {
|
||||||
"label": "Default character between lyrics",
|
"label": "Default character between lyrics",
|
||||||
"tooltip": "Choose the default character to use for the gap between lyrics"
|
"tooltip": "Choose the default character to use for the gap between lyrics"
|
||||||
|
|||||||
@ -843,6 +843,14 @@
|
|||||||
"label": "가사를 최대한 정교하게 동기화",
|
"label": "가사를 최대한 정교하게 동기화",
|
||||||
"tooltip": "다음 줄의 표시를 밀리초 단위로 계산합니다 (성능에 약간의 영향을 미칠 수 있음)"
|
"tooltip": "다음 줄의 표시를 밀리초 단위로 계산합니다 (성능에 약간의 영향을 미칠 수 있음)"
|
||||||
},
|
},
|
||||||
|
"preferred-provider": {
|
||||||
|
"label": "선호하는 가사 제공자",
|
||||||
|
"none": {
|
||||||
|
"label": "없음",
|
||||||
|
"tooltip": "선호하는 가사 제공자 없음"
|
||||||
|
},
|
||||||
|
"tooltip": "사용할 기본 가사 제공자를 선택하세요"
|
||||||
|
},
|
||||||
"romanization": {
|
"romanization": {
|
||||||
"label": "가사 로마자 변환",
|
"label": "가사 로마자 변환",
|
||||||
"tooltip": "가사가 영어가 아닌 언어로 되어있는 경우, 로마자 표기를 표시합니다."
|
"tooltip": "가사가 영어가 아닌 언어로 되어있는 경우, 로마자 표기를 표시합니다."
|
||||||
|
|||||||
@ -70,6 +70,7 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
// Custom
|
// Custom
|
||||||
init(backendCtx) {
|
init(backendCtx) {
|
||||||
this.app = new Hono();
|
this.app = new Hono();
|
||||||
|
|
||||||
const ws = createNodeWebSocket({
|
const ws = createNodeWebSocket({
|
||||||
app: this.app,
|
app: this.app,
|
||||||
});
|
});
|
||||||
@ -121,7 +122,7 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
() => this.volumeState,
|
() => this.volumeState,
|
||||||
);
|
);
|
||||||
registerAuth(this.app, backendCtx);
|
registerAuth(this.app, backendCtx);
|
||||||
registerWebsocket(this.app, ws);
|
registerWebsocket(this.app, backendCtx, ws);
|
||||||
|
|
||||||
// swagger
|
// swagger
|
||||||
this.app.openAPIRegistry.registerComponent(
|
this.app.openAPIRegistry.registerComponent(
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { ipcMain } from 'electron';
|
|
||||||
import { createRoute } from '@hono/zod-openapi';
|
import { createRoute } from '@hono/zod-openapi';
|
||||||
|
|
||||||
import { type NodeWebSocket } from '@hono/node-ws';
|
import { type NodeWebSocket } from '@hono/node-ws';
|
||||||
@ -15,6 +14,8 @@ import type { WSContext } from 'hono/ws';
|
|||||||
import type { Context, Next } from 'hono';
|
import type { Context, Next } from 'hono';
|
||||||
import type { RepeatMode, VolumeState } from '@/types/datahost-get-state';
|
import type { RepeatMode, VolumeState } from '@/types/datahost-get-state';
|
||||||
import type { HonoApp } from '../types';
|
import type { HonoApp } from '../types';
|
||||||
|
import type { BackendContext } from '@/types/contexts';
|
||||||
|
import type { APIServerConfig } from '@/plugins/api-server/config';
|
||||||
|
|
||||||
enum DataTypes {
|
enum DataTypes {
|
||||||
PlayerInfo = 'PLAYER_INFO',
|
PlayerInfo = 'PLAYER_INFO',
|
||||||
@ -36,7 +37,11 @@ type PlayerState = {
|
|||||||
shuffle: boolean;
|
shuffle: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
export const register = (
|
||||||
|
app: HonoApp,
|
||||||
|
{ ipc }: BackendContext<APIServerConfig>,
|
||||||
|
{ upgradeWebSocket }: NodeWebSocket,
|
||||||
|
) => {
|
||||||
let volumeState: VolumeState | undefined = undefined;
|
let volumeState: VolumeState | undefined = undefined;
|
||||||
let repeat: RepeatMode = 'NONE';
|
let repeat: RepeatMode = 'NONE';
|
||||||
let shuffle = false;
|
let shuffle = false;
|
||||||
@ -89,7 +94,7 @@ export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
|||||||
lastSongInfo = { ...songInfo };
|
lastSongInfo = { ...songInfo };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('ytmd:volume-changed', (_, newVolumeState: VolumeState) => {
|
ipc.on('ytmd:volume-changed', (newVolumeState: VolumeState) => {
|
||||||
volumeState = newVolumeState;
|
volumeState = newVolumeState;
|
||||||
send(DataTypes.VolumeChanged, {
|
send(DataTypes.VolumeChanged, {
|
||||||
volume: volumeState.state,
|
volume: volumeState.state,
|
||||||
@ -97,16 +102,16 @@ export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('ytmd:repeat-changed', (_, mode: RepeatMode) => {
|
ipc.on('ytmd:repeat-changed', (mode: RepeatMode) => {
|
||||||
repeat = mode;
|
repeat = mode;
|
||||||
send(DataTypes.RepeatChanged, { repeat });
|
send(DataTypes.RepeatChanged, { repeat });
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('ytmd:seeked', (_, t: number) => {
|
ipc.on('ytmd:seeked', (t: number) => {
|
||||||
send(DataTypes.PositionChanged, { position: t });
|
send(DataTypes.PositionChanged, { position: t });
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('ytmd:shuffle-changed', (_, newShuffle: boolean) => {
|
ipc.on('ytmd:shuffle-changed', (newShuffle: boolean) => {
|
||||||
shuffle = newShuffle;
|
shuffle = newShuffle;
|
||||||
send(DataTypes.ShuffleChanged, { shuffle });
|
send(DataTypes.ShuffleChanged, { shuffle });
|
||||||
});
|
});
|
||||||
@ -123,7 +128,7 @@ export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
nodeWebSocket.upgradeWebSocket(() => ({
|
upgradeWebSocket(() => ({
|
||||||
onOpen(_, ws) {
|
onOpen(_, ws) {
|
||||||
// "Unsafe argument of type `WSContext<WebSocket>` assigned to a parameter of type `WSContext<WebSocket>`. (@typescript-eslint/no-unsafe-argument)" ????? what?
|
// "Unsafe argument of type `WSContext<WebSocket>` assigned to a parameter of type `WSContext<WebSocket>`. (@typescript-eslint/no-unsafe-argument)" ????? what?
|
||||||
sockets.add(ws as WSContext<WebSocket>);
|
sockets.add(ws as WSContext<WebSocket>);
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { providerNames } from './providers';
|
||||||
|
|
||||||
import type { MenuItemConstructorOptions } from 'electron';
|
import type { MenuItemConstructorOptions } from 'electron';
|
||||||
import type { MenuContext } from '@/types/contexts';
|
import type { MenuContext } from '@/types/contexts';
|
||||||
import type { SyncedLyricsPluginConfig } from './types';
|
import type { SyncedLyricsPluginConfig } from './types';
|
||||||
@ -10,6 +12,35 @@ export const menu = async (
|
|||||||
const config = await ctx.getConfig();
|
const config = await ctx.getConfig();
|
||||||
|
|
||||||
return [
|
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'),
|
label: t('plugins.synced-lyrics.menu.precise-timing.label'),
|
||||||
toolTip: t('plugins.synced-lyrics.menu.precise-timing.tooltip'),
|
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';
|
export enum ProviderNames {
|
||||||
import { LyricsGenius } from './LyricsGenius';
|
YTMusic = 'YTMusic',
|
||||||
import { MusixMatch } from './MusixMatch';
|
LRCLib = 'LRCLib',
|
||||||
import { YTMusic } from './YTMusic';
|
MusixMatch = 'MusixMatch',
|
||||||
|
LyricsGenius = 'LyricsGenius',
|
||||||
|
// Megalobiz = 'Megalobiz',
|
||||||
|
}
|
||||||
|
|
||||||
import { getSongInfo } from '@/providers/song-info-front';
|
export const ProviderNameSchema = z.enum(ProviderNames);
|
||||||
|
export type ProviderName = z.infer<typeof ProviderNameSchema>;
|
||||||
import type { LyricProvider, LyricResult } from '../types';
|
export const providerNames = ProviderNameSchema.options;
|
||||||
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 type ProviderState = {
|
export type ProviderState = {
|
||||||
state: 'fetching' | 'done' | 'error';
|
state: 'fetching' | 'done' | 'error';
|
||||||
data: LyricResult | null;
|
data: LyricResult | null;
|
||||||
error: Error | 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 { getSongInfo } from '@/providers/song-info-front';
|
||||||
|
|
||||||
import { lyricsStore, retrySearch } from '../../providers';
|
import { lyricsStore, retrySearch } from '../store';
|
||||||
|
|
||||||
interface ErrorDisplayProps {
|
interface ErrorDisplayProps {
|
||||||
error: Error;
|
error: Error;
|
||||||
|
|||||||
@ -11,18 +11,24 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
} from 'solid-js';
|
} from 'solid-js';
|
||||||
|
|
||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
currentLyrics,
|
|
||||||
lyricsStore,
|
|
||||||
type ProviderName,
|
type ProviderName,
|
||||||
providerNames,
|
providerNames,
|
||||||
|
ProviderNameSchema,
|
||||||
type ProviderState,
|
type ProviderState,
|
||||||
setLyricsStore,
|
|
||||||
} from '../../providers';
|
} from '../../providers';
|
||||||
|
import { currentLyrics, lyricsStore, setLyricsStore } from '../store';
|
||||||
import { _ytAPI } from '../index';
|
import { _ytAPI } from '../index';
|
||||||
|
import { config } from '../renderer';
|
||||||
|
|
||||||
import type { YtIcons } from '@/types/icons';
|
import type { YtIcons } from '@/types/icons';
|
||||||
|
import type { PlayerAPIEvents } from '@/types/player-api-events';
|
||||||
|
|
||||||
|
const LocalStorageSchema = z.object({
|
||||||
|
provider: ProviderNameSchema,
|
||||||
|
});
|
||||||
|
|
||||||
export const providerIdx = createMemo(() =>
|
export const providerIdx = createMemo(() =>
|
||||||
providerNames.indexOf(lyricsStore.provider),
|
providerNames.indexOf(lyricsStore.provider),
|
||||||
@ -45,11 +51,19 @@ const providerBias = (p: ProviderName) =>
|
|||||||
(lyricsStore.lyrics[p].data?.lyrics ? 1 : -1);
|
(lyricsStore.lyrics[p].data?.lyrics ? 1 : -1);
|
||||||
|
|
||||||
const pickBestProvider = () => {
|
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);
|
const providers = Array.from(providerNames);
|
||||||
|
|
||||||
providers.sort((a, b) => providerBias(b) - providerBias(a));
|
providers.sort((a, b) => providerBias(b) - providerBias(a));
|
||||||
|
|
||||||
return providers[0];
|
return { provider: providers[0], force: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
||||||
@ -58,34 +72,91 @@ const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] =
|
|||||||
export const LyricsPicker = (props: {
|
export const LyricsPicker = (props: {
|
||||||
setStickRef: Setter<HTMLElement | null>;
|
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(() => {
|
createEffect(() => {
|
||||||
// fallback to the next source, if the current one has an error
|
|
||||||
if (!hasManuallySwitchedProvider()) {
|
if (!hasManuallySwitchedProvider()) {
|
||||||
const bestProvider = pickBestProvider();
|
const starred = starredProvider();
|
||||||
|
if (starred !== null) {
|
||||||
|
setLyricsStore('provider', starred);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const allProvidersFailed = providerNames.every((p) =>
|
const allProvidersFailed = providerNames.every((p) =>
|
||||||
shouldSwitchProvider(lyricsStore.lyrics[p]),
|
shouldSwitchProvider(lyricsStore.lyrics[p]),
|
||||||
);
|
);
|
||||||
if (allProvidersFailed) return;
|
if (allProvidersFailed) return;
|
||||||
|
|
||||||
if (providerBias(lyricsStore.provider) < providerBias(bestProvider)) {
|
const { provider, force } = pickBestProvider();
|
||||||
setLyricsStore('provider', bestProvider);
|
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 = () => {
|
const next = () => {
|
||||||
setHasManuallySwitchedProvider(true);
|
setHasManuallySwitchedProvider(true);
|
||||||
setLyricsStore('provider', (prevProvider) => {
|
setLyricsStore('provider', (prevProvider) => {
|
||||||
@ -176,9 +247,9 @@ export const LyricsPicker = (props: {
|
|||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={currentLyrics().state === 'error'}>
|
<Match when={currentLyrics().state === 'error'}>
|
||||||
<yt-icon-button
|
<yt-icon
|
||||||
icon={errorIcon}
|
icon={errorIcon}
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.8)' }}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
@ -189,9 +260,9 @@ export const LyricsPicker = (props: {
|
|||||||
currentLyrics().data?.lyrics)
|
currentLyrics().data?.lyrics)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<yt-icon-button
|
<yt-icon
|
||||||
icon={successIcon}
|
icon={successIcon}
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.8)' }}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
@ -202,9 +273,9 @@ export const LyricsPicker = (props: {
|
|||||||
!currentLyrics().data?.lyrics
|
!currentLyrics().data?.lyrics
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<yt-icon-button
|
<yt-icon
|
||||||
icon={notFoundIcon}
|
icon={notFoundIcon}
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.8)' }}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
@ -213,6 +284,20 @@ export const LyricsPicker = (props: {
|
|||||||
class="description ytmusic-description-shelf-renderer"
|
class="description ytmusic-description-shelf-renderer"
|
||||||
text={{ runs: [{ text: provider() }] }}
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Index>
|
</Index>
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import { waitForElement } from '@/utils/wait-for-element';
|
|||||||
|
|
||||||
import { selectors, tabStates } from './utils';
|
import { selectors, tabStates } from './utils';
|
||||||
import { setConfig, setCurrentTime } from './renderer';
|
import { setConfig, setCurrentTime } from './renderer';
|
||||||
|
import { fetchLyrics } from './store';
|
||||||
import { fetchLyrics } from '../providers';
|
|
||||||
|
|
||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
PlainLyrics,
|
PlainLyrics,
|
||||||
} from './components';
|
} from './components';
|
||||||
|
|
||||||
import { currentLyrics } from '../providers';
|
import { currentLyrics } from './store';
|
||||||
|
|
||||||
import type { LineLyrics, SyncedLyricsPluginConfig } from '../types';
|
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 { SongInfo } from '@/providers/song-info';
|
||||||
|
import type { ProviderName } from './providers';
|
||||||
|
|
||||||
export type SyncedLyricsPluginConfig = {
|
export type SyncedLyricsPluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
preferredProvider?: ProviderName;
|
||||||
preciseTiming: boolean;
|
preciseTiming: boolean;
|
||||||
showTimeCodes: boolean;
|
showTimeCodes: boolean;
|
||||||
defaultTextString: string | string[];
|
defaultTextString: string | string[];
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const typeList = Object.values(MaterialType);
|
|||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: () => t('plugins.transparent-player.name'),
|
name: () => t('plugins.transparent-player.name'),
|
||||||
description: () => t('plugins.transparent-player.description'),
|
description: () => t('plugins.transparent-player.description'),
|
||||||
addedVersion: '3.10.x',
|
addedVersion: '3.11.x',
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
platform: Platform.Windows,
|
platform: Platform.Windows,
|
||||||
config: defaultConfig,
|
config: defaultConfig,
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import type {
|
|||||||
import type { SongInfo } from './song-info';
|
import type { SongInfo } from './song-info';
|
||||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||||
|
|
||||||
|
const DATAUPDATED_FALLBACK_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
let songInfo: SongInfo = {} as SongInfo;
|
let songInfo: SongInfo = {} as SongInfo;
|
||||||
export const getSongInfo = () => songInfo;
|
export const getSongInfo = () => songInfo;
|
||||||
|
|
||||||
@ -253,12 +255,25 @@ export const setupSongInfo = (api: YoutubePlayer) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const waitingEvent = new Set<string>();
|
const waitingEvent = new Set<string>();
|
||||||
|
const waitingTimeouts = new Map<string, NodeJS.Timeout>();
|
||||||
|
|
||||||
|
const clearVideoTimeout = (videoId: string) => {
|
||||||
|
const timeoutId = waitingTimeouts.get(videoId);
|
||||||
|
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
waitingTimeouts.delete(videoId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Name = "dataloaded" and abit later "dataupdated"
|
// Name = "dataloaded" and abit later "dataupdated"
|
||||||
|
// Sometimes "dataupdated" is not fired, so we need to fallback to "dataloaded"
|
||||||
api.addEventListener('videodatachange', (name, videoData) => {
|
api.addEventListener('videodatachange', (name, videoData) => {
|
||||||
videoEventDispatcher(name, videoData);
|
videoEventDispatcher(name, videoData);
|
||||||
|
|
||||||
if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) {
|
if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) {
|
||||||
waitingEvent.delete(videoData.videoId);
|
waitingEvent.delete(videoData.videoId);
|
||||||
|
clearVideoTimeout(videoData.videoId);
|
||||||
sendSongInfo(videoData);
|
sendSongInfo(videoData);
|
||||||
} else if (name === 'dataloaded') {
|
} else if (name === 'dataloaded') {
|
||||||
const video = document.querySelector<HTMLVideoElement>('video');
|
const video = document.querySelector<HTMLVideoElement>('video');
|
||||||
@ -269,7 +284,18 @@ export const setupSongInfo = (api: YoutubePlayer) => {
|
|||||||
video?.addEventListener(status, playPausedHandlers[status]);
|
video?.addEventListener(status, playPausedHandlers[status]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearVideoTimeout(videoData.videoId);
|
||||||
waitingEvent.add(videoData.videoId);
|
waitingEvent.add(videoData.videoId);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (waitingEvent.has(videoData.videoId)) {
|
||||||
|
waitingEvent.delete(videoData.videoId);
|
||||||
|
waitingTimeouts.delete(videoData.videoId);
|
||||||
|
sendSongInfo(videoData);
|
||||||
|
}
|
||||||
|
}, DATAUPDATED_FALLBACK_TIMEOUT_MS);
|
||||||
|
|
||||||
|
waitingTimeouts.set(videoData.videoId, timeoutId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
4
src/yt-web-components.d.ts
vendored
4
src/yt-web-components.d.ts
vendored
@ -48,8 +48,8 @@ declare module 'solid-js' {
|
|||||||
'tp-yt-paper-icon-button': ComponentProps<'div'> &
|
'tp-yt-paper-icon-button': ComponentProps<'div'> &
|
||||||
TpYtPaperIconButtonProps;
|
TpYtPaperIconButtonProps;
|
||||||
'yt-icon-button': ComponentProps<'div'> & TpYtPaperIconButtonProps;
|
'yt-icon-button': ComponentProps<'div'> & TpYtPaperIconButtonProps;
|
||||||
'tp-yt-iron-icon': ComponentProps<'div'>;
|
'tp-yt-iron-icon': ComponentProps<'div'> & TpYtPaperIconButtonProps;
|
||||||
'yt-icon': ComponentProps<'div'>;
|
'yt-icon': ComponentProps<'div'> & TpYtPaperIconButtonProps;
|
||||||
// input type="range" slider component
|
// input type="range" slider component
|
||||||
'tp-yt-paper-slider': ComponentProps<'input'> & {
|
'tp-yt-paper-slider': ComponentProps<'input'> & {
|
||||||
'value'?: number | string;
|
'value'?: number | string;
|
||||||
|
|||||||
Reference in New Issue
Block a user