@involvex/youtube-music-cli 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +352 -0
- package/dist/eslint.config.d.ts +2 -0
- package/dist/eslint.config.js +55 -0
- package/dist/source/app.d.ts +4 -0
- package/dist/source/app.js +17 -0
- package/dist/source/cli.d.ts +2 -0
- package/dist/source/cli.js +241 -0
- package/dist/source/components/common/ErrorBoundary.d.ts +15 -0
- package/dist/source/components/common/ErrorBoundary.js +22 -0
- package/dist/source/components/common/Help.d.ts +1 -0
- package/dist/source/components/common/Help.js +10 -0
- package/dist/source/components/common/ShortcutsBar.d.ts +1 -0
- package/dist/source/components/common/ShortcutsBar.js +33 -0
- package/dist/source/components/config/ConfigLayout.d.ts +1 -0
- package/dist/source/components/config/ConfigLayout.js +84 -0
- package/dist/source/components/layouts/MainLayout.d.ts +4 -0
- package/dist/source/components/layouts/MainLayout.js +83 -0
- package/dist/source/components/layouts/PlayerLayout.d.ts +1 -0
- package/dist/source/components/layouts/PlayerLayout.js +10 -0
- package/dist/source/components/layouts/PluginsLayout.d.ts +1 -0
- package/dist/source/components/layouts/PluginsLayout.js +77 -0
- package/dist/source/components/layouts/SearchLayout.d.ts +4 -0
- package/dist/source/components/layouts/SearchLayout.js +81 -0
- package/dist/source/components/player/NowPlaying.d.ts +1 -0
- package/dist/source/components/player/NowPlaying.js +21 -0
- package/dist/source/components/player/PlayerControls.d.ts +1 -0
- package/dist/source/components/player/PlayerControls.js +41 -0
- package/dist/source/components/player/ProgressBar.d.ts +1 -0
- package/dist/source/components/player/ProgressBar.js +18 -0
- package/dist/source/components/player/QueueList.d.ts +4 -0
- package/dist/source/components/player/QueueList.js +30 -0
- package/dist/source/components/player/Suggestions.d.ts +1 -0
- package/dist/source/components/player/Suggestions.js +47 -0
- package/dist/source/components/playlist/PlaylistList.d.ts +1 -0
- package/dist/source/components/playlist/PlaylistList.js +11 -0
- package/dist/source/components/plugins/PluginInstallDialog.d.ts +5 -0
- package/dist/source/components/plugins/PluginInstallDialog.js +41 -0
- package/dist/source/components/plugins/PluginsAvailable.d.ts +5 -0
- package/dist/source/components/plugins/PluginsAvailable.js +55 -0
- package/dist/source/components/plugins/PluginsList.d.ts +8 -0
- package/dist/source/components/plugins/PluginsList.js +18 -0
- package/dist/source/components/search/SearchBar.d.ts +8 -0
- package/dist/source/components/search/SearchBar.js +50 -0
- package/dist/source/components/search/SearchResults.d.ts +10 -0
- package/dist/source/components/search/SearchResults.js +111 -0
- package/dist/source/components/settings/Settings.d.ts +1 -0
- package/dist/source/components/settings/Settings.js +42 -0
- package/dist/source/components/theme/ThemeSwitcher.d.ts +1 -0
- package/dist/source/components/theme/ThemeSwitcher.js +11 -0
- package/dist/source/config/themes.config.d.ts +3 -0
- package/dist/source/config/themes.config.js +63 -0
- package/dist/source/contexts/theme.context.d.ts +13 -0
- package/dist/source/contexts/theme.context.js +29 -0
- package/dist/source/hooks/useKeyboard.d.ts +10 -0
- package/dist/source/hooks/useKeyboard.js +104 -0
- package/dist/source/hooks/useNavigation.d.ts +1 -0
- package/dist/source/hooks/useNavigation.js +5 -0
- package/dist/source/hooks/usePlayer.d.ts +23 -0
- package/dist/source/hooks/usePlayer.js +35 -0
- package/dist/source/hooks/usePlaylist.d.ts +8 -0
- package/dist/source/hooks/usePlaylist.js +50 -0
- package/dist/source/hooks/useSearch.d.ts +8 -0
- package/dist/source/hooks/useSearch.js +76 -0
- package/dist/source/hooks/useTerminalSize.d.ts +4 -0
- package/dist/source/hooks/useTerminalSize.js +24 -0
- package/dist/source/hooks/useTheme.d.ts +6 -0
- package/dist/source/hooks/useTheme.js +5 -0
- package/dist/source/hooks/useYouTubeMusic.d.ts +11 -0
- package/dist/source/hooks/useYouTubeMusic.js +112 -0
- package/dist/source/main.d.ts +4 -0
- package/dist/source/main.js +69 -0
- package/dist/source/services/config/config.service.d.ts +26 -0
- package/dist/source/services/config/config.service.js +125 -0
- package/dist/source/services/logger/logger.service.d.ts +10 -0
- package/dist/source/services/logger/logger.service.js +52 -0
- package/dist/source/services/player/player.service.d.ts +58 -0
- package/dist/source/services/player/player.service.js +349 -0
- package/dist/source/services/player-state/player-state.service.d.ts +24 -0
- package/dist/source/services/player-state/player-state.service.js +122 -0
- package/dist/source/services/plugin/plugin-audio-api.d.ts +17 -0
- package/dist/source/services/plugin/plugin-audio-api.js +36 -0
- package/dist/source/services/plugin/plugin-context.d.ts +5 -0
- package/dist/source/services/plugin/plugin-context.js +256 -0
- package/dist/source/services/plugin/plugin-hooks.service.d.ts +62 -0
- package/dist/source/services/plugin/plugin-hooks.service.js +135 -0
- package/dist/source/services/plugin/plugin-installer.service.d.ts +27 -0
- package/dist/source/services/plugin/plugin-installer.service.js +247 -0
- package/dist/source/services/plugin/plugin-loader.service.d.ts +33 -0
- package/dist/source/services/plugin/plugin-loader.service.js +161 -0
- package/dist/source/services/plugin/plugin-permissions.service.d.ts +72 -0
- package/dist/source/services/plugin/plugin-permissions.service.js +194 -0
- package/dist/source/services/plugin/plugin-registry.service.d.ts +76 -0
- package/dist/source/services/plugin/plugin-registry.service.js +215 -0
- package/dist/source/services/plugin/plugin-ui-api.d.ts +25 -0
- package/dist/source/services/plugin/plugin-ui-api.js +46 -0
- package/dist/source/services/plugin/plugin-updater.service.d.ts +23 -0
- package/dist/source/services/plugin/plugin-updater.service.js +206 -0
- package/dist/source/services/youtube-music/api.d.ts +13 -0
- package/dist/source/services/youtube-music/api.js +371 -0
- package/dist/source/services/youtube-music/search.service.d.ts +11 -0
- package/dist/source/services/youtube-music/search.service.js +38 -0
- package/dist/source/stores/navigation.store.d.ts +10 -0
- package/dist/source/stores/navigation.store.js +67 -0
- package/dist/source/stores/player.store.d.ts +28 -0
- package/dist/source/stores/player.store.js +458 -0
- package/dist/source/stores/plugins.store.d.ts +46 -0
- package/dist/source/stores/plugins.store.js +177 -0
- package/dist/source/types/actions.d.ts +119 -0
- package/dist/source/types/actions.js +1 -0
- package/dist/source/types/cli.types.d.ts +14 -0
- package/dist/source/types/cli.types.js +1 -0
- package/dist/source/types/config.types.d.ts +19 -0
- package/dist/source/types/config.types.js +1 -0
- package/dist/source/types/keyboard.types.d.ts +5 -0
- package/dist/source/types/keyboard.types.js +1 -0
- package/dist/source/types/navigation.types.d.ts +14 -0
- package/dist/source/types/navigation.types.js +1 -0
- package/dist/source/types/player.types.d.ts +16 -0
- package/dist/source/types/player.types.js +1 -0
- package/dist/source/types/playlist.types.d.ts +12 -0
- package/dist/source/types/playlist.types.js +1 -0
- package/dist/source/types/plugin.types.d.ts +239 -0
- package/dist/source/types/plugin.types.js +1 -0
- package/dist/source/types/theme.types.d.ts +18 -0
- package/dist/source/types/theme.types.js +1 -0
- package/dist/source/types/youtube-music.types.d.ts +35 -0
- package/dist/source/types/youtube-music.types.js +1 -0
- package/dist/source/types/youtubei.types.d.ts +60 -0
- package/dist/source/types/youtubei.types.js +3 -0
- package/dist/source/utils/constants.d.ts +65 -0
- package/dist/source/utils/constants.js +82 -0
- package/dist/source/utils/format.d.ts +3 -0
- package/dist/source/utils/format.js +24 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +13 -0
- package/package.json +100 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Player state persistence service
|
|
2
|
+
import { writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { CONFIG_DIR } from "../../utils/constants.js";
|
|
6
|
+
import { logger } from "../logger/logger.service.js";
|
|
7
|
+
const STATE_FILE = join(CONFIG_DIR, 'player-state.json');
|
|
8
|
+
const SCHEMA_VERSION = 1;
|
|
9
|
+
const defaultState = {
|
|
10
|
+
schemaVersion: SCHEMA_VERSION,
|
|
11
|
+
currentTrack: null,
|
|
12
|
+
queue: [],
|
|
13
|
+
queuePosition: 0,
|
|
14
|
+
progress: 0,
|
|
15
|
+
volume: 70,
|
|
16
|
+
shuffle: false,
|
|
17
|
+
repeat: 'off',
|
|
18
|
+
lastUpdated: new Date().toISOString(),
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Saves player state to disk
|
|
22
|
+
*/
|
|
23
|
+
export async function savePlayerState(state) {
|
|
24
|
+
try {
|
|
25
|
+
// Ensure config directory exists
|
|
26
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
27
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
28
|
+
logger.debug('PlayerStateService', 'Created config directory', {
|
|
29
|
+
path: CONFIG_DIR,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// Merge with default state
|
|
33
|
+
const stateToSave = {
|
|
34
|
+
...defaultState,
|
|
35
|
+
...state,
|
|
36
|
+
schemaVersion: SCHEMA_VERSION,
|
|
37
|
+
lastUpdated: new Date().toISOString(),
|
|
38
|
+
};
|
|
39
|
+
// Write to temporary file first, then rename for atomic write
|
|
40
|
+
const tempFile = `${STATE_FILE}.tmp`;
|
|
41
|
+
await writeFile(tempFile, JSON.stringify(stateToSave, null, 2), 'utf8');
|
|
42
|
+
// On Windows, we need to handle the rename differently
|
|
43
|
+
if (process.platform === 'win32' && existsSync(STATE_FILE)) {
|
|
44
|
+
// Delete existing file first on Windows
|
|
45
|
+
await import('node:fs/promises').then(async (fs) => {
|
|
46
|
+
await fs.unlink(STATE_FILE);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
await import('node:fs/promises').then(async (fs) => {
|
|
50
|
+
await fs.rename(tempFile, STATE_FILE);
|
|
51
|
+
});
|
|
52
|
+
logger.debug('PlayerStateService', 'Saved player state', {
|
|
53
|
+
hasTrack: !!stateToSave.currentTrack,
|
|
54
|
+
queueLength: stateToSave.queue.length,
|
|
55
|
+
progress: stateToSave.progress,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger.error('PlayerStateService', 'Failed to save player state', {
|
|
60
|
+
error: error instanceof Error ? error.message : String(error),
|
|
61
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Loads player state from disk
|
|
67
|
+
*/
|
|
68
|
+
export async function loadPlayerState() {
|
|
69
|
+
try {
|
|
70
|
+
if (!existsSync(STATE_FILE)) {
|
|
71
|
+
logger.debug('PlayerStateService', 'No saved state file found');
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const data = await readFile(STATE_FILE, 'utf8');
|
|
75
|
+
const state = JSON.parse(data);
|
|
76
|
+
// Validate schema version
|
|
77
|
+
if (state.schemaVersion !== SCHEMA_VERSION) {
|
|
78
|
+
logger.warn('PlayerStateService', 'Schema version mismatch', {
|
|
79
|
+
expected: SCHEMA_VERSION,
|
|
80
|
+
found: state.schemaVersion,
|
|
81
|
+
});
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
// Validate state structure
|
|
85
|
+
if (!state || typeof state !== 'object') {
|
|
86
|
+
logger.warn('PlayerStateService', 'Invalid state structure');
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
logger.info('PlayerStateService', 'Loaded player state', {
|
|
90
|
+
hasTrack: !!state.currentTrack,
|
|
91
|
+
queueLength: state.queue?.length ?? 0,
|
|
92
|
+
progress: state.progress,
|
|
93
|
+
lastUpdated: state.lastUpdated,
|
|
94
|
+
});
|
|
95
|
+
return state;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
logger.error('PlayerStateService', 'Failed to load player state', {
|
|
99
|
+
error: error instanceof Error ? error.message : String(error),
|
|
100
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
101
|
+
});
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Clears saved player state
|
|
107
|
+
*/
|
|
108
|
+
export async function clearPlayerState() {
|
|
109
|
+
try {
|
|
110
|
+
if (existsSync(STATE_FILE)) {
|
|
111
|
+
await import('node:fs/promises').then(async (fs) => {
|
|
112
|
+
await fs.unlink(STATE_FILE);
|
|
113
|
+
});
|
|
114
|
+
logger.info('PlayerStateService', 'Cleared player state');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.error('PlayerStateService', 'Failed to clear player state', {
|
|
119
|
+
error: error instanceof Error ? error.message : String(error),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Track } from '../../types/youtube-music.types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Transform audio URL through all enabled plugins
|
|
4
|
+
*/
|
|
5
|
+
export declare function transformAudioUrl(url: string, track: Track): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Notify plugins of stream start
|
|
8
|
+
*/
|
|
9
|
+
export declare function notifyStreamStart(url: string, track: Track): void;
|
|
10
|
+
/**
|
|
11
|
+
* Notify plugins of stream end
|
|
12
|
+
*/
|
|
13
|
+
export declare function notifyStreamEnd(track: Track): void;
|
|
14
|
+
/**
|
|
15
|
+
* Notify plugins of stream error
|
|
16
|
+
*/
|
|
17
|
+
export declare function notifyStreamError(error: Error, track?: Track): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Plugin audio API integration - hooks for audio stream modification
|
|
2
|
+
import { getPluginHooksService } from "./plugin-hooks.service.js";
|
|
3
|
+
import { logger } from "../logger/logger.service.js";
|
|
4
|
+
/**
|
|
5
|
+
* Transform audio URL through all enabled plugins
|
|
6
|
+
*/
|
|
7
|
+
export async function transformAudioUrl(url, track) {
|
|
8
|
+
const hooksService = getPluginHooksService();
|
|
9
|
+
// Emit stream-request event
|
|
10
|
+
await hooksService.emit(hooksService.createAudioStreamEvent('stream-request', { url, track }));
|
|
11
|
+
// For now, return original URL
|
|
12
|
+
// Plugins will register handlers that can modify this
|
|
13
|
+
return url;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Notify plugins of stream start
|
|
17
|
+
*/
|
|
18
|
+
export function notifyStreamStart(url, track) {
|
|
19
|
+
const hooksService = getPluginHooksService();
|
|
20
|
+
hooksService.emitSync(hooksService.createAudioStreamEvent('stream-start', { url, track }));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Notify plugins of stream end
|
|
24
|
+
*/
|
|
25
|
+
export function notifyStreamEnd(track) {
|
|
26
|
+
const hooksService = getPluginHooksService();
|
|
27
|
+
hooksService.emitSync(hooksService.createAudioStreamEvent('stream-end', { track }));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Notify plugins of stream error
|
|
31
|
+
*/
|
|
32
|
+
export function notifyStreamError(error, track) {
|
|
33
|
+
const hooksService = getPluginHooksService();
|
|
34
|
+
hooksService.emitSync(hooksService.createAudioStreamEvent('stream-error', { error, track }));
|
|
35
|
+
logger.error('AudioAPI', 'Stream error:', error);
|
|
36
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PluginContext, PluginManifest } from '../../types/plugin.types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Create a plugin context for a specific plugin
|
|
4
|
+
*/
|
|
5
|
+
export declare function createPluginContext(manifest: PluginManifest, playerAPI: PluginContext['player'], navigationAPI: PluginContext['navigation']): PluginContext;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { getPluginPermissionsService } from "./plugin-permissions.service.js";
|
|
2
|
+
import { getPluginHooksService } from "./plugin-hooks.service.js";
|
|
3
|
+
import { getConfigService } from "../config/config.service.js";
|
|
4
|
+
import { logger as appLogger } from "../logger/logger.service.js";
|
|
5
|
+
import { CONFIG_DIR } from "../../utils/constants.js";
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, rmSync, } from 'node:fs';
|
|
8
|
+
/**
|
|
9
|
+
* Create a plugin context for a specific plugin
|
|
10
|
+
*/
|
|
11
|
+
export function createPluginContext(manifest, playerAPI, navigationAPI) {
|
|
12
|
+
const permissionsService = getPluginPermissionsService();
|
|
13
|
+
const hooksService = getPluginHooksService();
|
|
14
|
+
const configService = getConfigService();
|
|
15
|
+
const pluginId = manifest.id;
|
|
16
|
+
// Plugin data directory
|
|
17
|
+
const pluginDataDir = join(CONFIG_DIR, 'plugins', pluginId, 'data');
|
|
18
|
+
// Ensure data directory exists
|
|
19
|
+
if (!existsSync(pluginDataDir)) {
|
|
20
|
+
mkdirSync(pluginDataDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
// Permission checker wrapper
|
|
23
|
+
const checkPermission = (permission) => {
|
|
24
|
+
if (!permissionsService.hasPermission(pluginId, permission)) {
|
|
25
|
+
throw new Error(`Plugin ${pluginId} does not have ${permission} permission`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
// Create scoped logger for plugin
|
|
29
|
+
const pluginLogger = {
|
|
30
|
+
debug: (message, ...args) => {
|
|
31
|
+
appLogger.debug(`[${manifest.name}]`, message, ...args);
|
|
32
|
+
},
|
|
33
|
+
info: (message, ...args) => {
|
|
34
|
+
appLogger.info(`[${manifest.name}]`, message, ...args);
|
|
35
|
+
},
|
|
36
|
+
warn: (message, ...args) => {
|
|
37
|
+
appLogger.warn(`[${manifest.name}]`, message, ...args);
|
|
38
|
+
},
|
|
39
|
+
error: (message, ...args) => {
|
|
40
|
+
appLogger.error(`[${manifest.name}]`, message, ...args);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
// Filesystem API (scoped to plugin data directory)
|
|
44
|
+
const filesystemAPI = {
|
|
45
|
+
readFile: async (path) => {
|
|
46
|
+
checkPermission('filesystem');
|
|
47
|
+
const fullPath = join(pluginDataDir, path);
|
|
48
|
+
return readFileSync(fullPath, 'utf-8');
|
|
49
|
+
},
|
|
50
|
+
writeFile: async (path, data) => {
|
|
51
|
+
checkPermission('filesystem');
|
|
52
|
+
const fullPath = join(pluginDataDir, path);
|
|
53
|
+
// Ensure parent directory exists
|
|
54
|
+
const dir = join(fullPath, '..');
|
|
55
|
+
if (!existsSync(dir)) {
|
|
56
|
+
mkdirSync(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
writeFileSync(fullPath, data, 'utf-8');
|
|
59
|
+
},
|
|
60
|
+
deleteFile: async (path) => {
|
|
61
|
+
checkPermission('filesystem');
|
|
62
|
+
const fullPath = join(pluginDataDir, path);
|
|
63
|
+
if (existsSync(fullPath)) {
|
|
64
|
+
rmSync(fullPath);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
exists: async (path) => {
|
|
68
|
+
checkPermission('filesystem');
|
|
69
|
+
const fullPath = join(pluginDataDir, path);
|
|
70
|
+
return existsSync(fullPath);
|
|
71
|
+
},
|
|
72
|
+
listFiles: async (path = '') => {
|
|
73
|
+
checkPermission('filesystem');
|
|
74
|
+
const fullPath = join(pluginDataDir, path);
|
|
75
|
+
if (!existsSync(fullPath)) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
return readdirSync(fullPath);
|
|
79
|
+
},
|
|
80
|
+
getDataDir: () => {
|
|
81
|
+
return pluginDataDir;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
// Config API (scoped to plugin config namespace)
|
|
85
|
+
const pluginConfigKey = `plugin.${pluginId}`;
|
|
86
|
+
const configAPI = {
|
|
87
|
+
get: (key, defaultValue) => {
|
|
88
|
+
const fullKey = `${pluginConfigKey}.${key}`;
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
+
const value = configService.get(fullKey);
|
|
91
|
+
return (value ?? defaultValue);
|
|
92
|
+
},
|
|
93
|
+
set: (key, value) => {
|
|
94
|
+
const fullKey = `${pluginConfigKey}.${key}`;
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
configService.set(fullKey, value);
|
|
97
|
+
},
|
|
98
|
+
delete: (key) => {
|
|
99
|
+
const fullKey = `${pluginConfigKey}.${key}`;
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
configService.set(fullKey, undefined);
|
|
102
|
+
},
|
|
103
|
+
getAll: () => {
|
|
104
|
+
// Get all keys starting with plugin config namespace
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
const allConfig = configService.get('pluginConfigs');
|
|
107
|
+
return allConfig?.[pluginId] ?? {};
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
// Audio API
|
|
111
|
+
const audioAPI = {
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
113
|
+
transformStreamUrl: async (url, _track) => {
|
|
114
|
+
checkPermission('player');
|
|
115
|
+
// Plugins can return modified URL, null to skip, or same URL
|
|
116
|
+
return url;
|
|
117
|
+
},
|
|
118
|
+
onStreamRequest: handler => {
|
|
119
|
+
checkPermission('player');
|
|
120
|
+
// Register handler for stream requests
|
|
121
|
+
hooksService.on('stream-request', async (event) => {
|
|
122
|
+
if (event.type === 'stream-request' && event.url && event.track) {
|
|
123
|
+
await handler(event.url, event.track);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
// Player API with permission checks
|
|
129
|
+
const wrappedPlayerAPI = {
|
|
130
|
+
play: async (track) => {
|
|
131
|
+
checkPermission('player');
|
|
132
|
+
return playerAPI.play(track);
|
|
133
|
+
},
|
|
134
|
+
pause: () => {
|
|
135
|
+
checkPermission('player');
|
|
136
|
+
playerAPI.pause();
|
|
137
|
+
},
|
|
138
|
+
resume: () => {
|
|
139
|
+
checkPermission('player');
|
|
140
|
+
playerAPI.resume();
|
|
141
|
+
},
|
|
142
|
+
stop: () => {
|
|
143
|
+
checkPermission('player');
|
|
144
|
+
playerAPI.stop();
|
|
145
|
+
},
|
|
146
|
+
next: () => {
|
|
147
|
+
checkPermission('player');
|
|
148
|
+
playerAPI.next();
|
|
149
|
+
},
|
|
150
|
+
previous: () => {
|
|
151
|
+
checkPermission('player');
|
|
152
|
+
playerAPI.previous();
|
|
153
|
+
},
|
|
154
|
+
seek: (position) => {
|
|
155
|
+
checkPermission('player');
|
|
156
|
+
playerAPI.seek(position);
|
|
157
|
+
},
|
|
158
|
+
setVolume: (volume) => {
|
|
159
|
+
checkPermission('player');
|
|
160
|
+
playerAPI.setVolume(volume);
|
|
161
|
+
},
|
|
162
|
+
getVolume: () => {
|
|
163
|
+
checkPermission('player');
|
|
164
|
+
return playerAPI.getVolume();
|
|
165
|
+
},
|
|
166
|
+
getCurrentTrack: () => {
|
|
167
|
+
checkPermission('player');
|
|
168
|
+
return playerAPI.getCurrentTrack();
|
|
169
|
+
},
|
|
170
|
+
getQueue: () => {
|
|
171
|
+
checkPermission('player');
|
|
172
|
+
return playerAPI.getQueue();
|
|
173
|
+
},
|
|
174
|
+
addToQueue: (track) => {
|
|
175
|
+
checkPermission('player');
|
|
176
|
+
playerAPI.addToQueue(track);
|
|
177
|
+
},
|
|
178
|
+
removeFromQueue: (index) => {
|
|
179
|
+
checkPermission('player');
|
|
180
|
+
playerAPI.removeFromQueue(index);
|
|
181
|
+
},
|
|
182
|
+
clearQueue: () => {
|
|
183
|
+
checkPermission('player');
|
|
184
|
+
playerAPI.clearQueue();
|
|
185
|
+
},
|
|
186
|
+
shuffle: (enabled) => {
|
|
187
|
+
checkPermission('player');
|
|
188
|
+
playerAPI.shuffle(enabled);
|
|
189
|
+
},
|
|
190
|
+
setRepeat: (mode) => {
|
|
191
|
+
checkPermission('player');
|
|
192
|
+
playerAPI.setRepeat(mode);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
// Navigation API with permission checks
|
|
196
|
+
const wrappedNavigationAPI = {
|
|
197
|
+
navigate: (view) => {
|
|
198
|
+
checkPermission('ui');
|
|
199
|
+
navigationAPI.navigate(view);
|
|
200
|
+
},
|
|
201
|
+
goBack: () => {
|
|
202
|
+
checkPermission('ui');
|
|
203
|
+
navigationAPI.goBack();
|
|
204
|
+
},
|
|
205
|
+
getCurrentView: () => {
|
|
206
|
+
checkPermission('ui');
|
|
207
|
+
return navigationAPI.getCurrentView();
|
|
208
|
+
},
|
|
209
|
+
registerView: (viewId, component) => {
|
|
210
|
+
checkPermission('ui');
|
|
211
|
+
navigationAPI.registerView(viewId, component);
|
|
212
|
+
},
|
|
213
|
+
unregisterView: (viewId) => {
|
|
214
|
+
checkPermission('ui');
|
|
215
|
+
navigationAPI.unregisterView(viewId);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
// Event system
|
|
219
|
+
const context = {
|
|
220
|
+
plugin: manifest,
|
|
221
|
+
player: wrappedPlayerAPI,
|
|
222
|
+
navigation: wrappedNavigationAPI,
|
|
223
|
+
config: configAPI,
|
|
224
|
+
logger: pluginLogger,
|
|
225
|
+
filesystem: filesystemAPI,
|
|
226
|
+
audio: audioAPI,
|
|
227
|
+
on: (eventType, handler) => {
|
|
228
|
+
hooksService.on(eventType, handler);
|
|
229
|
+
},
|
|
230
|
+
off: (eventType, handler) => {
|
|
231
|
+
hooksService.off(eventType, handler);
|
|
232
|
+
},
|
|
233
|
+
emit: (event) => {
|
|
234
|
+
hooksService.emitSync(event);
|
|
235
|
+
},
|
|
236
|
+
hasPermission: (permission) => {
|
|
237
|
+
return permissionsService.hasPermission(pluginId, permission);
|
|
238
|
+
},
|
|
239
|
+
requestPermission: async (permission) => {
|
|
240
|
+
return permissionsService.requestPermission(pluginId, permission);
|
|
241
|
+
},
|
|
242
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
243
|
+
registerShortcut: (_keys, _handler) => {
|
|
244
|
+
checkPermission('ui');
|
|
245
|
+
// This will be implemented when we integrate with useKeyBinding
|
|
246
|
+
pluginLogger.warn('registerShortcut not yet implemented');
|
|
247
|
+
},
|
|
248
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
249
|
+
unregisterShortcut: (_keys) => {
|
|
250
|
+
checkPermission('ui');
|
|
251
|
+
// This will be implemented when we integrate with useKeyBinding
|
|
252
|
+
pluginLogger.warn('unregisterShortcut not yet implemented');
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
return context;
|
|
256
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PluginEvent, EventHandler, PlayerEvent, NavigationEvent, AudioStreamEvent } from '../../types/plugin.types.ts';
|
|
2
|
+
type EventType = PluginEvent['type'];
|
|
3
|
+
/**
|
|
4
|
+
* Plugin hooks service - manages event subscriptions and emissions
|
|
5
|
+
*/
|
|
6
|
+
declare class PluginHooksService {
|
|
7
|
+
private handlers;
|
|
8
|
+
constructor();
|
|
9
|
+
/**
|
|
10
|
+
* Register an event handler
|
|
11
|
+
*/
|
|
12
|
+
on<T extends PluginEvent = PluginEvent>(eventType: T['type'], handler: EventHandler<T>): void;
|
|
13
|
+
/**
|
|
14
|
+
* Unregister an event handler
|
|
15
|
+
*/
|
|
16
|
+
off<T extends PluginEvent = PluginEvent>(eventType: T['type'], handler: EventHandler<T>): void;
|
|
17
|
+
/**
|
|
18
|
+
* Emit an event to all registered handlers
|
|
19
|
+
*/
|
|
20
|
+
emit<T extends PluginEvent = PluginEvent>(event: T): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Emit event synchronously (fire and forget)
|
|
23
|
+
*/
|
|
24
|
+
emitSync<T extends PluginEvent = PluginEvent>(event: T): void;
|
|
25
|
+
/**
|
|
26
|
+
* Remove all handlers for a specific event type
|
|
27
|
+
*/
|
|
28
|
+
clearHandlers(eventType: EventType): void;
|
|
29
|
+
/**
|
|
30
|
+
* Remove all event handlers (used for cleanup)
|
|
31
|
+
*/
|
|
32
|
+
clearAllHandlers(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Get count of handlers for an event type
|
|
35
|
+
*/
|
|
36
|
+
getHandlerCount(eventType: EventType): number;
|
|
37
|
+
/**
|
|
38
|
+
* Get all registered event types
|
|
39
|
+
*/
|
|
40
|
+
getRegisteredEvents(): EventType[];
|
|
41
|
+
/**
|
|
42
|
+
* Helper: Create a player event
|
|
43
|
+
*/
|
|
44
|
+
createPlayerEvent(type: PlayerEvent['type'], data?: Partial<Omit<PlayerEvent, 'type' | 'timestamp'>>): PlayerEvent;
|
|
45
|
+
/**
|
|
46
|
+
* Helper: Create a navigation event
|
|
47
|
+
*/
|
|
48
|
+
createNavigationEvent(type: NavigationEvent['type'], data?: Partial<Omit<NavigationEvent, 'type' | 'timestamp'>>): NavigationEvent;
|
|
49
|
+
/**
|
|
50
|
+
* Helper: Create an audio stream event
|
|
51
|
+
*/
|
|
52
|
+
createAudioStreamEvent(type: AudioStreamEvent['type'], data?: Partial<Omit<AudioStreamEvent, 'type' | 'timestamp'>>): AudioStreamEvent;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the plugin hooks service singleton
|
|
56
|
+
*/
|
|
57
|
+
export declare function getPluginHooksService(): PluginHooksService;
|
|
58
|
+
/**
|
|
59
|
+
* Reset the singleton (for testing)
|
|
60
|
+
*/
|
|
61
|
+
export declare function resetPluginHooksService(): void;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { logger } from "../logger/logger.service.js";
|
|
2
|
+
/**
|
|
3
|
+
* Plugin hooks service - manages event subscriptions and emissions
|
|
4
|
+
*/
|
|
5
|
+
class PluginHooksService {
|
|
6
|
+
handlers;
|
|
7
|
+
constructor() {
|
|
8
|
+
this.handlers = new Map();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Register an event handler
|
|
12
|
+
*/
|
|
13
|
+
on(eventType, handler) {
|
|
14
|
+
if (!this.handlers.has(eventType)) {
|
|
15
|
+
this.handlers.set(eventType, new Set());
|
|
16
|
+
}
|
|
17
|
+
this.handlers.get(eventType).add(handler);
|
|
18
|
+
logger.debug('PluginHooksService', `Registered handler for ${eventType}`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Unregister an event handler
|
|
22
|
+
*/
|
|
23
|
+
off(eventType, handler) {
|
|
24
|
+
const handlers = this.handlers.get(eventType);
|
|
25
|
+
if (handlers) {
|
|
26
|
+
handlers.delete(handler);
|
|
27
|
+
if (handlers.size === 0) {
|
|
28
|
+
this.handlers.delete(eventType);
|
|
29
|
+
}
|
|
30
|
+
logger.debug('PluginHooksService', `Unregistered handler for ${eventType}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Emit an event to all registered handlers
|
|
35
|
+
*/
|
|
36
|
+
async emit(event) {
|
|
37
|
+
const handlers = this.handlers.get(event.type);
|
|
38
|
+
if (!handlers || handlers.size === 0) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
logger.debug('PluginHooksService', `Emitting ${event.type} to ${handlers.size} handler(s)`);
|
|
42
|
+
// Execute all handlers, catching errors to prevent one plugin from breaking others
|
|
43
|
+
const promises = [];
|
|
44
|
+
for (const handler of handlers) {
|
|
45
|
+
promises.push(Promise.resolve()
|
|
46
|
+
.then(() => handler(event))
|
|
47
|
+
.catch((error) => {
|
|
48
|
+
logger.error('PluginHooksService', `Error in handler for ${event.type}:`, error);
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
await Promise.all(promises);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Emit event synchronously (fire and forget)
|
|
55
|
+
*/
|
|
56
|
+
emitSync(event) {
|
|
57
|
+
void this.emit(event);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Remove all handlers for a specific event type
|
|
61
|
+
*/
|
|
62
|
+
clearHandlers(eventType) {
|
|
63
|
+
this.handlers.delete(eventType);
|
|
64
|
+
logger.debug('PluginHooksService', `Cleared all handlers for ${eventType}`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Remove all event handlers (used for cleanup)
|
|
68
|
+
*/
|
|
69
|
+
clearAllHandlers() {
|
|
70
|
+
this.handlers.clear();
|
|
71
|
+
logger.debug('PluginHooksService', 'Cleared all event handlers');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get count of handlers for an event type
|
|
75
|
+
*/
|
|
76
|
+
getHandlerCount(eventType) {
|
|
77
|
+
return this.handlers.get(eventType)?.size ?? 0;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get all registered event types
|
|
81
|
+
*/
|
|
82
|
+
getRegisteredEvents() {
|
|
83
|
+
return [...this.handlers.keys()];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Helper: Create a player event
|
|
87
|
+
*/
|
|
88
|
+
createPlayerEvent(type, data) {
|
|
89
|
+
return {
|
|
90
|
+
type,
|
|
91
|
+
...data,
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Helper: Create a navigation event
|
|
97
|
+
*/
|
|
98
|
+
createNavigationEvent(type, data) {
|
|
99
|
+
return {
|
|
100
|
+
type,
|
|
101
|
+
...data,
|
|
102
|
+
timestamp: Date.now(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Helper: Create an audio stream event
|
|
107
|
+
*/
|
|
108
|
+
createAudioStreamEvent(type, data) {
|
|
109
|
+
return {
|
|
110
|
+
type,
|
|
111
|
+
...data,
|
|
112
|
+
timestamp: Date.now(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Singleton instance
|
|
117
|
+
let instance = null;
|
|
118
|
+
/**
|
|
119
|
+
* Get the plugin hooks service singleton
|
|
120
|
+
*/
|
|
121
|
+
export function getPluginHooksService() {
|
|
122
|
+
if (!instance) {
|
|
123
|
+
instance = new PluginHooksService();
|
|
124
|
+
}
|
|
125
|
+
return instance;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Reset the singleton (for testing)
|
|
129
|
+
*/
|
|
130
|
+
export function resetPluginHooksService() {
|
|
131
|
+
if (instance) {
|
|
132
|
+
instance.clearAllHandlers();
|
|
133
|
+
}
|
|
134
|
+
instance = null;
|
|
135
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PluginInstallResult } from '../../types/plugin.types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Plugin installer service
|
|
4
|
+
*/
|
|
5
|
+
declare class PluginInstallerService {
|
|
6
|
+
/**
|
|
7
|
+
* Install a plugin from GitHub repository
|
|
8
|
+
*/
|
|
9
|
+
installFromGitHub(repoUrl: string, pluginName?: string): Promise<PluginInstallResult>;
|
|
10
|
+
/**
|
|
11
|
+
* Install a plugin from the default plugin repository
|
|
12
|
+
*/
|
|
13
|
+
installFromDefaultRepo(pluginName: string): Promise<PluginInstallResult>;
|
|
14
|
+
/**
|
|
15
|
+
* Install a plugin from local directory (for development)
|
|
16
|
+
*/
|
|
17
|
+
installFromLocal(sourcePath: string): Promise<PluginInstallResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Uninstall a plugin
|
|
20
|
+
*/
|
|
21
|
+
uninstall(pluginId: string): Promise<PluginInstallResult>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get the plugin installer service singleton
|
|
25
|
+
*/
|
|
26
|
+
export declare function getPluginInstallerService(): PluginInstallerService;
|
|
27
|
+
export {};
|