@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.
Files changed (136) hide show
  1. package/README.md +352 -0
  2. package/dist/eslint.config.d.ts +2 -0
  3. package/dist/eslint.config.js +55 -0
  4. package/dist/source/app.d.ts +4 -0
  5. package/dist/source/app.js +17 -0
  6. package/dist/source/cli.d.ts +2 -0
  7. package/dist/source/cli.js +241 -0
  8. package/dist/source/components/common/ErrorBoundary.d.ts +15 -0
  9. package/dist/source/components/common/ErrorBoundary.js +22 -0
  10. package/dist/source/components/common/Help.d.ts +1 -0
  11. package/dist/source/components/common/Help.js +10 -0
  12. package/dist/source/components/common/ShortcutsBar.d.ts +1 -0
  13. package/dist/source/components/common/ShortcutsBar.js +33 -0
  14. package/dist/source/components/config/ConfigLayout.d.ts +1 -0
  15. package/dist/source/components/config/ConfigLayout.js +84 -0
  16. package/dist/source/components/layouts/MainLayout.d.ts +4 -0
  17. package/dist/source/components/layouts/MainLayout.js +83 -0
  18. package/dist/source/components/layouts/PlayerLayout.d.ts +1 -0
  19. package/dist/source/components/layouts/PlayerLayout.js +10 -0
  20. package/dist/source/components/layouts/PluginsLayout.d.ts +1 -0
  21. package/dist/source/components/layouts/PluginsLayout.js +77 -0
  22. package/dist/source/components/layouts/SearchLayout.d.ts +4 -0
  23. package/dist/source/components/layouts/SearchLayout.js +81 -0
  24. package/dist/source/components/player/NowPlaying.d.ts +1 -0
  25. package/dist/source/components/player/NowPlaying.js +21 -0
  26. package/dist/source/components/player/PlayerControls.d.ts +1 -0
  27. package/dist/source/components/player/PlayerControls.js +41 -0
  28. package/dist/source/components/player/ProgressBar.d.ts +1 -0
  29. package/dist/source/components/player/ProgressBar.js +18 -0
  30. package/dist/source/components/player/QueueList.d.ts +4 -0
  31. package/dist/source/components/player/QueueList.js +30 -0
  32. package/dist/source/components/player/Suggestions.d.ts +1 -0
  33. package/dist/source/components/player/Suggestions.js +47 -0
  34. package/dist/source/components/playlist/PlaylistList.d.ts +1 -0
  35. package/dist/source/components/playlist/PlaylistList.js +11 -0
  36. package/dist/source/components/plugins/PluginInstallDialog.d.ts +5 -0
  37. package/dist/source/components/plugins/PluginInstallDialog.js +41 -0
  38. package/dist/source/components/plugins/PluginsAvailable.d.ts +5 -0
  39. package/dist/source/components/plugins/PluginsAvailable.js +55 -0
  40. package/dist/source/components/plugins/PluginsList.d.ts +8 -0
  41. package/dist/source/components/plugins/PluginsList.js +18 -0
  42. package/dist/source/components/search/SearchBar.d.ts +8 -0
  43. package/dist/source/components/search/SearchBar.js +50 -0
  44. package/dist/source/components/search/SearchResults.d.ts +10 -0
  45. package/dist/source/components/search/SearchResults.js +111 -0
  46. package/dist/source/components/settings/Settings.d.ts +1 -0
  47. package/dist/source/components/settings/Settings.js +42 -0
  48. package/dist/source/components/theme/ThemeSwitcher.d.ts +1 -0
  49. package/dist/source/components/theme/ThemeSwitcher.js +11 -0
  50. package/dist/source/config/themes.config.d.ts +3 -0
  51. package/dist/source/config/themes.config.js +63 -0
  52. package/dist/source/contexts/theme.context.d.ts +13 -0
  53. package/dist/source/contexts/theme.context.js +29 -0
  54. package/dist/source/hooks/useKeyboard.d.ts +10 -0
  55. package/dist/source/hooks/useKeyboard.js +104 -0
  56. package/dist/source/hooks/useNavigation.d.ts +1 -0
  57. package/dist/source/hooks/useNavigation.js +5 -0
  58. package/dist/source/hooks/usePlayer.d.ts +23 -0
  59. package/dist/source/hooks/usePlayer.js +35 -0
  60. package/dist/source/hooks/usePlaylist.d.ts +8 -0
  61. package/dist/source/hooks/usePlaylist.js +50 -0
  62. package/dist/source/hooks/useSearch.d.ts +8 -0
  63. package/dist/source/hooks/useSearch.js +76 -0
  64. package/dist/source/hooks/useTerminalSize.d.ts +4 -0
  65. package/dist/source/hooks/useTerminalSize.js +24 -0
  66. package/dist/source/hooks/useTheme.d.ts +6 -0
  67. package/dist/source/hooks/useTheme.js +5 -0
  68. package/dist/source/hooks/useYouTubeMusic.d.ts +11 -0
  69. package/dist/source/hooks/useYouTubeMusic.js +112 -0
  70. package/dist/source/main.d.ts +4 -0
  71. package/dist/source/main.js +69 -0
  72. package/dist/source/services/config/config.service.d.ts +26 -0
  73. package/dist/source/services/config/config.service.js +125 -0
  74. package/dist/source/services/logger/logger.service.d.ts +10 -0
  75. package/dist/source/services/logger/logger.service.js +52 -0
  76. package/dist/source/services/player/player.service.d.ts +58 -0
  77. package/dist/source/services/player/player.service.js +349 -0
  78. package/dist/source/services/player-state/player-state.service.d.ts +24 -0
  79. package/dist/source/services/player-state/player-state.service.js +122 -0
  80. package/dist/source/services/plugin/plugin-audio-api.d.ts +17 -0
  81. package/dist/source/services/plugin/plugin-audio-api.js +36 -0
  82. package/dist/source/services/plugin/plugin-context.d.ts +5 -0
  83. package/dist/source/services/plugin/plugin-context.js +256 -0
  84. package/dist/source/services/plugin/plugin-hooks.service.d.ts +62 -0
  85. package/dist/source/services/plugin/plugin-hooks.service.js +135 -0
  86. package/dist/source/services/plugin/plugin-installer.service.d.ts +27 -0
  87. package/dist/source/services/plugin/plugin-installer.service.js +247 -0
  88. package/dist/source/services/plugin/plugin-loader.service.d.ts +33 -0
  89. package/dist/source/services/plugin/plugin-loader.service.js +161 -0
  90. package/dist/source/services/plugin/plugin-permissions.service.d.ts +72 -0
  91. package/dist/source/services/plugin/plugin-permissions.service.js +194 -0
  92. package/dist/source/services/plugin/plugin-registry.service.d.ts +76 -0
  93. package/dist/source/services/plugin/plugin-registry.service.js +215 -0
  94. package/dist/source/services/plugin/plugin-ui-api.d.ts +25 -0
  95. package/dist/source/services/plugin/plugin-ui-api.js +46 -0
  96. package/dist/source/services/plugin/plugin-updater.service.d.ts +23 -0
  97. package/dist/source/services/plugin/plugin-updater.service.js +206 -0
  98. package/dist/source/services/youtube-music/api.d.ts +13 -0
  99. package/dist/source/services/youtube-music/api.js +371 -0
  100. package/dist/source/services/youtube-music/search.service.d.ts +11 -0
  101. package/dist/source/services/youtube-music/search.service.js +38 -0
  102. package/dist/source/stores/navigation.store.d.ts +10 -0
  103. package/dist/source/stores/navigation.store.js +67 -0
  104. package/dist/source/stores/player.store.d.ts +28 -0
  105. package/dist/source/stores/player.store.js +458 -0
  106. package/dist/source/stores/plugins.store.d.ts +46 -0
  107. package/dist/source/stores/plugins.store.js +177 -0
  108. package/dist/source/types/actions.d.ts +119 -0
  109. package/dist/source/types/actions.js +1 -0
  110. package/dist/source/types/cli.types.d.ts +14 -0
  111. package/dist/source/types/cli.types.js +1 -0
  112. package/dist/source/types/config.types.d.ts +19 -0
  113. package/dist/source/types/config.types.js +1 -0
  114. package/dist/source/types/keyboard.types.d.ts +5 -0
  115. package/dist/source/types/keyboard.types.js +1 -0
  116. package/dist/source/types/navigation.types.d.ts +14 -0
  117. package/dist/source/types/navigation.types.js +1 -0
  118. package/dist/source/types/player.types.d.ts +16 -0
  119. package/dist/source/types/player.types.js +1 -0
  120. package/dist/source/types/playlist.types.d.ts +12 -0
  121. package/dist/source/types/playlist.types.js +1 -0
  122. package/dist/source/types/plugin.types.d.ts +239 -0
  123. package/dist/source/types/plugin.types.js +1 -0
  124. package/dist/source/types/theme.types.d.ts +18 -0
  125. package/dist/source/types/theme.types.js +1 -0
  126. package/dist/source/types/youtube-music.types.d.ts +35 -0
  127. package/dist/source/types/youtube-music.types.js +1 -0
  128. package/dist/source/types/youtubei.types.d.ts +60 -0
  129. package/dist/source/types/youtubei.types.js +3 -0
  130. package/dist/source/utils/constants.d.ts +65 -0
  131. package/dist/source/utils/constants.js +82 -0
  132. package/dist/source/utils/format.d.ts +3 -0
  133. package/dist/source/utils/format.js +24 -0
  134. package/dist/test.d.ts +1 -0
  135. package/dist/test.js +13 -0
  136. 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 {};