fix: load plugins

Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
JellyBrick
2023-11-28 00:34:36 +09:00
parent 2fe28cf126
commit 7a76079ff4
15 changed files with 90 additions and 39 deletions

View File

@ -138,6 +138,7 @@
"@cliqz/adblocker-electron": "1.26.11", "@cliqz/adblocker-electron": "1.26.11",
"@cliqz/adblocker-electron-preload": "1.26.11", "@cliqz/adblocker-electron-preload": "1.26.11",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@electron/remote": "2.1.0",
"@ffmpeg.wasm/core-mt": "0.12.0", "@ffmpeg.wasm/core-mt": "0.12.0",
"@ffmpeg.wasm/main": "0.12.0", "@ffmpeg.wasm/main": "0.12.0",
"@foobar404/wave": "2.0.4", "@foobar404/wave": "2.0.4",

11
pnpm-lock.yaml generated
View File

@ -22,6 +22,9 @@ dependencies:
'@electron-toolkit/tsconfig': '@electron-toolkit/tsconfig':
specifier: ^1.0.1 specifier: ^1.0.1
version: 1.0.1(@types/node@20.8.6) version: 1.0.1(@types/node@20.8.6)
'@electron/remote':
specifier: 2.1.0
version: 2.1.0(electron@27.0.4)
'@ffmpeg.wasm/core-mt': '@ffmpeg.wasm/core-mt':
specifier: 0.12.0 specifier: 0.12.0
version: 0.12.0 version: 0.12.0
@ -568,6 +571,14 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@electron/remote@2.1.0(electron@27.0.4):
resolution: {integrity: sha512-38jzz2beoYTo0DNS+aoaGyLS/fHeNTAc1Aom6HlYsxKnvVWjcg4xriC7J2IUkYSEDHGKX/D7jUst+mH4dHR6QA==}
peerDependencies:
electron: '>= 13.0.0'
dependencies:
electron: 27.0.4
dev: false
/@electron/universal@1.4.5: /@electron/universal@1.4.5:
resolution: {integrity: sha512-3vE9WBQnvlulKylrPbyc+9M4xnD7t1JxuCOF0nrFz00XrrkgbqeqxDf90PNcjLiuB4hAZKr1JooVA6KwsXj94w==} resolution: {integrity: sha512-3vE9WBQnvlulKylrPbyc+9M4xnD7t1JxuCOF0nrFz00XrrkgbqeqxDf90PNcjLiuB4hAZKr1JooVA6KwsXj94w==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}

View File

@ -163,13 +163,7 @@ const migrations = {
export default new Store({ export default new Store({
defaults: { defaults: {
...defaults, ...defaults,
plugins: Object.entries(allPlugins).reduce( // README: 'plugin' uses deepmerge to populate the default values, so it is not necessary to include it here
(prev, [id, plugin]) => ({
...prev,
[id]: plugin.config,
}),
{},
),
}, },
clearInvalidConfig: false, clearInvalidConfig: false,
migrations, migrations,

View File

@ -163,7 +163,7 @@ const initHook = (win: BrowserWindow) => {
const mainPlugin = getAllLoadedMainPlugins()[id]; const mainPlugin = getAllLoadedMainPlugins()[id];
if (mainPlugin) { if (mainPlugin) {
if (config.enabled && typeof mainPlugin.backend !== 'function') { if (config.enabled && typeof mainPlugin.backend !== 'function') {
mainPlugin.backend?.onConfigChange?.(config); mainPlugin.backend?.onConfigChange?.bind(mainPlugin.backend)?.(config);
} }
} }

View File

@ -96,16 +96,18 @@ export const forceLoadMainPlugin = async (
loadedPluginMap[id] = plugin; loadedPluginMap[id] = plugin;
resolve(); resolve();
} catch (err) { } catch (err) {
console.log( console.error(
'[YTMusic]', '[YTMusic]',
`Cannot initialize "${id}" plugin: ${String(err)}`, `Cannot initialize "${id}" plugin: `,
); );
console.trace(err);
reject(err); reject(err);
} }
}); });
}; };
export const loadAllMainPlugins = async (win: BrowserWindow) => { export const loadAllMainPlugins = async (win: BrowserWindow) => {
console.log('[YTMusic]', 'Loading all plugins');
const pluginConfigs = config.plugins.getPlugins(); const pluginConfigs = config.plugins.getPlugins();
const queue: Promise<void>[] = []; const queue: Promise<void>[] = [];
@ -118,7 +120,7 @@ export const loadAllMainPlugins = async (win: BrowserWindow) => {
} }
} }
await Promise.all(queue); await Promise.allSettled(queue);
}; };
export const unloadAllMainPlugins = (win: BrowserWindow) => { export const unloadAllMainPlugins = (win: BrowserWindow) => {

View File

@ -35,7 +35,8 @@ export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => {
console.log('[YTMusic]', `Successfully loaded '${id}::menu'`); console.log('[YTMusic]', `Successfully loaded '${id}::menu'`);
} catch (err) { } catch (err) {
console.log('[YTMusic]', `Cannot initialize '${id}::menu': ${String(err)}`); console.error('[YTMusic]', `Cannot initialize '${id}::menu': `);
console.trace(err);
} }
}; };

View File

@ -42,10 +42,11 @@ export const forceLoadPreloadPlugin = (id: string) => {
console.log('[YTMusic]', `"${id}" plugin is loaded`); console.log('[YTMusic]', `"${id}" plugin is loaded`);
} catch (err) { } catch (err) {
console.log( console.error(
'[YTMusic]', '[YTMusic]',
`Cannot initialize "${id}" plugin: ${String(err)}`, `Cannot initialize "${id}" plugin: `,
); );
console.trace(err);
} }
}; };

View File

@ -28,12 +28,11 @@ interface CaptionsSelectorConfig {
lastCaptionsCode: string; lastCaptionsCode: string;
} }
const captionsSettingsButton = ElementFromHtml(CaptionsSettingsButtonHTML);
export default createPlugin< export default createPlugin<
unknown, unknown,
unknown, unknown,
{ {
captionsSettingsButton: HTMLElement;
captionTrackList: LanguageOptions[] | null; captionTrackList: LanguageOptions[] | null;
api: YoutubePlayer | null; api: YoutubePlayer | null;
config: CaptionsSelectorConfig | null; config: CaptionsSelectorConfig | null;
@ -98,6 +97,7 @@ export default createPlugin<
}, },
renderer: { renderer: {
captionsSettingsButton: ElementFromHtml(CaptionsSettingsButtonHTML),
captionTrackList: null, captionTrackList: null,
api: null, api: null,
config: null, config: null,
@ -133,7 +133,7 @@ export default createPlugin<
captionsButtonClickListener() { captionsButtonClickListener() {
if (this.config!.disableCaptions) { if (this.config!.disableCaptions) {
setTimeout(() => this.api!.unloadModule('captions'), 100); setTimeout(() => this.api!.unloadModule('captions'), 100);
captionsSettingsButton.style.display = 'none'; this.captionsSettingsButton.style.display = 'none';
return; return;
} }
@ -148,7 +148,7 @@ export default createPlugin<
}); });
} }
captionsSettingsButton.style.display = this.captionTrackList?.length this.captionsSettingsButton.style.display = this.captionTrackList?.length
? 'inline-block' ? 'inline-block'
: 'none'; : 'none';
}, 250); }, 250);
@ -158,20 +158,20 @@ export default createPlugin<
this.setConfig = setConfig; this.setConfig = setConfig;
}, },
stop() { stop() {
document.querySelector('.right-controls-buttons')?.removeChild(captionsSettingsButton); document.querySelector('.right-controls-buttons')?.removeChild(this.captionsSettingsButton);
document.querySelector<YoutubePlayer & HTMLElement>('#movie_player')?.unloadModule('captions'); document.querySelector<YoutubePlayer & HTMLElement>('#movie_player')?.unloadModule('captions');
document.querySelector('video')?.removeEventListener('srcChanged', this.videoChangeListener); document.querySelector('video')?.removeEventListener('srcChanged', this.videoChangeListener);
captionsSettingsButton.removeEventListener('click', this.captionsButtonClickListener); this.captionsSettingsButton.removeEventListener('click', this.captionsButtonClickListener);
}, },
onPlayerApiReady(playerApi) { onPlayerApiReady(playerApi) {
this.api = playerApi; this.api = playerApi;
document.querySelector('.right-controls-buttons')?.append(captionsSettingsButton); document.querySelector('.right-controls-buttons')?.append(this.captionsSettingsButton);
this.captionTrackList = this.api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? []; this.captionTrackList = this.api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? [];
document.querySelector('video')?.addEventListener('srcChanged', this.videoChangeListener); document.querySelector('video')?.addEventListener('srcChanged', this.videoChangeListener);
captionsSettingsButton.addEventListener('click', this.captionsButtonClickListener); this.captionsSettingsButton.addEventListener('click', this.captionsButtonClickListener);
}, },
onConfigChange(newConfig) { onConfigChange(newConfig) {
this.config = newConfig; this.config = newConfig;

View File

@ -36,7 +36,7 @@ import { cache } from '@/providers/decorators';
import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types'; import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types';
import { defaultConfig, type DownloaderPluginConfig } from '../index'; import type { DownloaderPluginConfig } from '../index';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
@ -91,7 +91,7 @@ export const getCookieFromWindow = async (win: BrowserWindow) => {
.join(';'); .join(';');
}; };
let config: DownloaderPluginConfig = defaultConfig; let config: DownloaderPluginConfig;
export const onMainLoad = async ({ window: _win, getConfig, ipc }: BackendContext<DownloaderPluginConfig>) => { export const onMainLoad = async ({ window: _win, getConfig, ipc }: BackendContext<DownloaderPluginConfig>) => {
win = _win; win = _win;

View File

@ -5,12 +5,12 @@ import is from 'electron-is';
import { notificationImage } from './utils'; import { notificationImage } from './utils';
import interactive from './interactive'; import interactive from './interactive';
import { defaultConfig, type NotificationsPluginConfig } from './index';
import registerCallback, { type SongInfo } from '@/providers/song-info'; import registerCallback, { type SongInfo } from '@/providers/song-info';
import type { NotificationsPluginConfig } from './index';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
let config: NotificationsPluginConfig = defaultConfig; let config: NotificationsPluginConfig;
const notify = (info: SongInfo) => { const notify = (info: SongInfo) => {
// Send the notification // Send the notification

View File

@ -78,7 +78,7 @@ function onApiLoaded() {
Object.entries(getAllLoadedRendererPlugins()) Object.entries(getAllLoadedRendererPlugins())
.forEach(([id, plugin]) => { .forEach(([id, plugin]) => {
if (typeof plugin.renderer !== 'function') { if (typeof plugin.renderer !== 'function') {
plugin.renderer?.onPlayerApiReady?.(api!, createContext(id)); plugin.renderer?.onPlayerApiReady?.bind(plugin.renderer)?.(api!, createContext(id));
} }
}); });
@ -135,7 +135,7 @@ function onApiLoaded() {
if (api) { if (api) {
const plugin = getLoadedRendererPlugin(id); const plugin = getLoadedRendererPlugin(id);
if (plugin && typeof plugin.renderer !== 'function') { if (plugin && typeof plugin.renderer !== 'function') {
plugin.renderer?.onPlayerApiReady?.(api, createContext(id)); plugin.renderer?.onPlayerApiReady?.bind(plugin.renderer)?.(api, createContext(id));
} }
} }
}, },
@ -146,7 +146,7 @@ function onApiLoaded() {
(_event, id: string, newConfig: PluginConfig) => { (_event, id: string, newConfig: PluginConfig) => {
const plugin = getAllLoadedRendererPlugins()[id]; const plugin = getAllLoadedRendererPlugins()[id];
if (plugin && typeof plugin.renderer !== 'function') { if (plugin && typeof plugin.renderer !== 'function') {
plugin.renderer?.onConfigChange?.(newConfig); plugin.renderer?.onConfigChange?.bind(plugin.renderer)?.(newConfig);
} }
}, },
); );

View File

@ -42,7 +42,7 @@ export const startPlugin = <Config extends PluginConfig>(id: string, def: Plugin
try { try {
const start = performance.now(); const start = performance.now();
lifecycle(options.context as Config & typeof options.context); lifecycle.bind(def[options.ctx])(options.context as Config & typeof options.context);
console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`); console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`);
@ -62,7 +62,7 @@ export const stopPlugin = <Config extends PluginConfig>(id: string, def: PluginD
try { try {
const start = performance.now(); const start = performance.now();
stop(options.context as Config & typeof options.context); stop.bind(def[options.ctx])(options.context as Config & typeof options.context);
console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`); console.log(`[YTM] Executed ${id}::${options.ctx} in ${performance.now() - start} ms`);

View File

@ -4,7 +4,6 @@ declare module 'virtual:plugins' {
type Plugin = PluginDef<unknown, unknown, unknown, PluginConfig>; type Plugin = PluginDef<unknown, unknown, unknown, PluginConfig>;
export const mainPlugins: Record<string, Plugin>; export const mainPlugins: Record<string, Plugin>;
export const menuPlugins: Record<string, Plugin>;
export const preloadPlugins: Record<string, Plugin>; export const preloadPlugins: Record<string, Plugin>;
export const rendererPlugins: Record<string, Plugin>; export const rendererPlugins: Record<string, Plugin>;

View File

@ -7,7 +7,7 @@ const snakeToCamel = (text: string) =>
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase()); text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
export const pluginVirtualModuleGenerator = ( export const pluginVirtualModuleGenerator = (
mode: 'main' | 'preload' | 'renderer' | 'menu', mode: 'main' | 'preload' | 'renderer',
) => { ) => {
const project = new Project({ const project = new Project({
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'), tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
@ -37,7 +37,7 @@ export const pluginVirtualModuleGenerator = (
// prettier-ignore // prettier-ignore
for (const { name, path } of plugins) { for (const { name, path } of plugins) {
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/'); const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
writer.writeLine(`import ${snakeToCamel(name)}Plugin from "./${relativePath}";`); writer.writeLine(`import ${snakeToCamel(name)}Plugin, { pluginStub as ${snakeToCamel(name)}PluginStub } from "./${relativePath}";`);
} }
writer.blankLine(); writer.blankLine();
@ -45,15 +45,17 @@ export const pluginVirtualModuleGenerator = (
// Context-specific exports // Context-specific exports
writer.writeLine(`export const ${mode}Plugins = {`); writer.writeLine(`export const ${mode}Plugins = {`);
for (const { name } of plugins) { for (const { name } of plugins) {
writer.writeLine(` "${name}": ${snakeToCamel(name)}Plugin,`); const checkMode = mode === 'main' ? 'backend' : mode;
// HACK: To avoid situation like importing renderer plugins in main
writer.writeLine(` ...(${snakeToCamel(name)}Plugin['${checkMode}'] ? { "${name}": ${snakeToCamel(name)}Plugin } : {}),`);
} }
writer.writeLine('};'); writer.writeLine('};');
writer.blankLine(); writer.blankLine();
// All plugins export // All plugins export (stub only) // Omit<Plugin, 'backend' | 'preload' | 'renderer'>
writer.writeLine('export const allPlugins = {'); writer.writeLine('export const allPlugins = {');
for (const { name } of plugins) { for (const { name } of plugins) {
writer.writeLine(` "${name}": ${snakeToCamel(name)}Plugin,`); writer.writeLine(` "${name}": ${snakeToCamel(name)}PluginStub,`);
} }
writer.writeLine('};'); writer.writeLine('};');
writer.blankLine(); writer.blankLine();

View File

@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
import { resolve, basename } from 'node:path'; import { resolve, basename } from 'node:path';
import { createFilter } from 'vite'; import { createFilter } from 'vite';
import { Project, ts, ObjectLiteralExpression } from 'ts-morph'; import { Project, ts, ObjectLiteralExpression, VariableDeclarationKind } from 'ts-morph';
import type { PluginOption } from 'vite'; import type { PluginOption } from 'vite';
@ -78,7 +78,7 @@ export default function (mode: 'backend' | 'preload' | 'renderer' | 'none'): Plu
} }
}); });
const contexts = ['backend', 'preload', 'renderer']; const contexts = ['backend', 'preload', 'renderer', 'menu'];
for (const ctx of contexts) { for (const ctx of contexts) {
if (mode === 'none') { if (mode === 'none') {
const index = propertyNames.indexOf(ctx); const index = propertyNames.indexOf(ctx);
@ -89,6 +89,7 @@ export default function (mode: 'backend' | 'preload' | 'renderer' | 'none'): Plu
} }
if (ctx === mode) continue; if (ctx === mode) continue;
if (ctx === 'menu' && mode === 'backend') continue;
const index = propertyNames.indexOf(ctx); const index = propertyNames.indexOf(ctx);
if (index === -1) continue; if (index === -1) continue;
@ -96,6 +97,45 @@ export default function (mode: 'backend' | 'preload' | 'renderer' | 'none'): Plu
objExpr.getProperty(propertyNames[index])?.remove(); objExpr.getProperty(propertyNames[index])?.remove();
} }
const stubObjExpr = src.addVariableStatement({
isExported: true,
declarationKind: VariableDeclarationKind.Const,
declarations: [{
name: 'pluginStub',
initializer: (writer) => writer.write(objExpr!.getText()),
}]
})
.getDeclarations()[0]
.getInitializer() as ObjectLiteralExpression;
const stubProperties = stubObjExpr.getProperties();
const stubPropertyNames = stubProperties.map((prop) => {
switch (prop.getKind()) {
case ts.SyntaxKind.PropertyAssignment:
return prop
.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
.getName();
case ts.SyntaxKind.ShorthandPropertyAssignment:
return prop
.asKindOrThrow(ts.SyntaxKind.ShorthandPropertyAssignment)
.getName();
case ts.SyntaxKind.MethodDeclaration:
return prop
.asKindOrThrow(ts.SyntaxKind.MethodDeclaration)
.getName();
default:
throw new Error('Not implemented');
}
});
if (mode === 'backend') contexts.pop();
for (const ctx of contexts) {
const index = stubPropertyNames.indexOf(ctx);
if (index === -1) continue;
stubObjExpr.getProperty(stubPropertyNames[index])?.remove();
}
return { return {
code: src.getText(), code: src.getText(),
}; };