@involvex/youtube-music-cli 0.0.18 → 0.0.20

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 (36) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/source/cli.js +123 -8
  3. package/dist/source/components/import/ImportLayout.d.ts +1 -0
  4. package/dist/source/components/import/ImportLayout.js +119 -0
  5. package/dist/source/components/import/ImportProgress.d.ts +6 -0
  6. package/dist/source/components/import/ImportProgress.js +73 -0
  7. package/dist/source/components/layouts/MainLayout.js +15 -2
  8. package/dist/source/components/settings/Settings.js +6 -2
  9. package/dist/source/services/config/config.service.js +10 -0
  10. package/dist/source/services/import/import.service.d.ts +44 -0
  11. package/dist/source/services/import/import.service.js +272 -0
  12. package/dist/source/services/import/spotify.service.d.ts +40 -0
  13. package/dist/source/services/import/spotify.service.js +171 -0
  14. package/dist/source/services/import/track-matcher.service.d.ts +60 -0
  15. package/dist/source/services/import/track-matcher.service.js +271 -0
  16. package/dist/source/services/import/youtube-import.service.d.ts +17 -0
  17. package/dist/source/services/import/youtube-import.service.js +84 -0
  18. package/dist/source/services/web/static-file.service.d.ts +31 -0
  19. package/dist/source/services/web/static-file.service.js +163 -0
  20. package/dist/source/services/web/web-server-manager.d.ts +88 -0
  21. package/dist/source/services/web/web-server-manager.js +502 -0
  22. package/dist/source/services/web/web-streaming.service.d.ts +88 -0
  23. package/dist/source/services/web/web-streaming.service.js +290 -0
  24. package/dist/source/services/web/websocket.server.d.ts +63 -0
  25. package/dist/source/services/web/websocket.server.js +267 -0
  26. package/dist/source/stores/player.store.js +24 -0
  27. package/dist/source/types/cli.types.d.ts +8 -0
  28. package/dist/source/types/config.types.d.ts +2 -0
  29. package/dist/source/types/import.types.d.ts +72 -0
  30. package/dist/source/types/import.types.js +2 -0
  31. package/dist/source/types/web.types.d.ts +127 -0
  32. package/dist/source/types/web.types.js +2 -0
  33. package/dist/source/utils/constants.d.ts +1 -0
  34. package/dist/source/utils/constants.js +1 -0
  35. package/dist/youtube-music-cli.exe +0 -0
  36. package/package.json +8 -3
@@ -0,0 +1,502 @@
1
+ import { getWebSocketServer } from "./websocket.server.js";
2
+ import { getWebStreamingService } from "./web-streaming.service.js";
3
+ import { getConfigService } from "../config/config.service.js";
4
+ import { getImportService } from "../import/import.service.js";
5
+ import { getPlayerService } from "../player/player.service.js";
6
+ import { getSearchService } from "../youtube-music/search.service.js";
7
+ import { logger } from "../logger/logger.service.js";
8
+ class WebServerManager {
9
+ config;
10
+ isRunning = false;
11
+ cleanupHooks = [];
12
+ // Internal state for web-only mode (when PlayerProvider is not mounted)
13
+ internalState = {
14
+ currentTrack: null,
15
+ isPlaying: false,
16
+ volume: 70,
17
+ speed: 1,
18
+ progress: 0,
19
+ duration: 0,
20
+ queue: [],
21
+ queuePosition: 0,
22
+ repeat: 'off',
23
+ shuffle: false,
24
+ isLoading: false,
25
+ error: null,
26
+ };
27
+ constructor() {
28
+ // Load config or use defaults
29
+ const configService = getConfigService();
30
+ const savedConfig = configService.get('webServer');
31
+ this.config = savedConfig ?? {
32
+ enabled: false,
33
+ host: 'localhost',
34
+ port: 8080,
35
+ enableCors: true,
36
+ allowedOrigins: ['*'],
37
+ auth: { enabled: false },
38
+ };
39
+ // Save default config if not present
40
+ if (!savedConfig) {
41
+ configService.set('webServer', this.config);
42
+ }
43
+ // Initialize volume from config
44
+ this.internalState.volume = configService.get('volume') ?? 70;
45
+ }
46
+ /**
47
+ * Start the web server
48
+ */
49
+ async start(options) {
50
+ if (this.isRunning) {
51
+ logger.warn('WebServerManager', 'Server already running');
52
+ return;
53
+ }
54
+ // Apply CLI options
55
+ const finalConfig = { ...this.config };
56
+ if (options) {
57
+ if (options.host !== undefined) {
58
+ finalConfig.host = options.host;
59
+ }
60
+ if (options.port !== undefined) {
61
+ finalConfig.port = options.port;
62
+ }
63
+ if (options.auth !== undefined) {
64
+ finalConfig.auth.enabled = true;
65
+ finalConfig.auth.token = options.auth;
66
+ }
67
+ }
68
+ logger.info('WebServerManager', 'Starting web server', finalConfig);
69
+ try {
70
+ const wsServer = getWebSocketServer();
71
+ // Set up command handler
72
+ const cleanupCommand = this.setupCommandHandler();
73
+ this.cleanupHooks.push(cleanupCommand);
74
+ // Set up import handler
75
+ const cleanupImport = this.setupImportHandler();
76
+ this.cleanupHooks.push(cleanupImport);
77
+ // Start the server
78
+ await wsServer.start({
79
+ config: finalConfig,
80
+ onCommand: this.handleCommand.bind(this),
81
+ onImportRequest: this.handleImportRequest.bind(this),
82
+ onSearchRequest: this.handleSearchRequest.bind(this),
83
+ onConfigUpdate: this.handleConfigUpdate.bind(this),
84
+ });
85
+ this.isRunning = true;
86
+ // Set up graceful shutdown
87
+ this.setupShutdownHooks();
88
+ }
89
+ catch (error) {
90
+ logger.error('WebServerManager', 'Failed to start server', {
91
+ error: error instanceof Error ? error.message : String(error),
92
+ });
93
+ throw error;
94
+ }
95
+ }
96
+ /**
97
+ * Stop the web server
98
+ */
99
+ async stop() {
100
+ if (!this.isRunning) {
101
+ return;
102
+ }
103
+ logger.info('WebServerManager', 'Stopping web server');
104
+ // Clean up hooks
105
+ for (const cleanup of this.cleanupHooks) {
106
+ cleanup();
107
+ }
108
+ this.cleanupHooks = [];
109
+ // Stop the WebSocket server
110
+ const wsServer = getWebSocketServer();
111
+ await wsServer.stop();
112
+ this.isRunning = false;
113
+ }
114
+ /**
115
+ * Set up player command handler
116
+ */
117
+ setupCommandHandler() {
118
+ const streamingService = getWebStreamingService();
119
+ const unsubscribe = streamingService.onMessage(message => {
120
+ if (message.type === 'command') {
121
+ this.handleCommand(message.action);
122
+ }
123
+ });
124
+ return unsubscribe;
125
+ }
126
+ /**
127
+ * Set up import progress handler
128
+ */
129
+ setupImportHandler() {
130
+ const importService = getImportService();
131
+ const streamingService = getWebStreamingService();
132
+ const unsubscribe = importService.onProgress(progress => {
133
+ streamingService.onImportProgress(progress);
134
+ });
135
+ return unsubscribe;
136
+ }
137
+ /**
138
+ * Handle command from web client
139
+ */
140
+ handleCommand(action) {
141
+ logger.debug('WebServerManager', 'Executing command from client', { action });
142
+ const playerService = getPlayerService();
143
+ // Execute command and update internal state
144
+ switch (action.category) {
145
+ case 'PLAY': {
146
+ if (action.track) {
147
+ this.internalState.currentTrack = action.track;
148
+ this.internalState.isPlaying = true;
149
+ this.internalState.progress = 0;
150
+ this.internalState.error = null;
151
+ const youtubeUrl = `https://www.youtube.com/watch?v=${action.track.videoId}`;
152
+ void playerService.play(youtubeUrl, {
153
+ volume: this.internalState.volume,
154
+ });
155
+ }
156
+ break;
157
+ }
158
+ case 'PAUSE':
159
+ this.internalState.isPlaying = false;
160
+ playerService.pause();
161
+ break;
162
+ case 'RESUME':
163
+ this.internalState.isPlaying = true;
164
+ playerService.resume();
165
+ break;
166
+ case 'STOP':
167
+ this.internalState.isPlaying = false;
168
+ this.internalState.progress = 0;
169
+ this.internalState.currentTrack = null;
170
+ playerService.stop();
171
+ break;
172
+ case 'NEXT': {
173
+ if (this.internalState.queue.length === 0)
174
+ break;
175
+ if (this.internalState.shuffle && this.internalState.queue.length > 1) {
176
+ let randomIndex;
177
+ do {
178
+ randomIndex = Math.floor(Math.random() * this.internalState.queue.length);
179
+ } while (randomIndex === this.internalState.queuePosition);
180
+ this.internalState.queuePosition = randomIndex;
181
+ }
182
+ else {
183
+ const nextPosition = this.internalState.queuePosition + 1;
184
+ if (nextPosition >= this.internalState.queue.length) {
185
+ if (this.internalState.repeat === 'all') {
186
+ this.internalState.queuePosition = 0;
187
+ }
188
+ else {
189
+ break;
190
+ }
191
+ }
192
+ else {
193
+ this.internalState.queuePosition = nextPosition;
194
+ }
195
+ }
196
+ this.internalState.currentTrack =
197
+ this.internalState.queue[this.internalState.queuePosition] ?? null;
198
+ this.internalState.isPlaying = true;
199
+ this.internalState.progress = 0;
200
+ if (this.internalState.currentTrack) {
201
+ const youtubeUrl = `https://www.youtube.com/watch?v=${this.internalState.currentTrack.videoId}`;
202
+ void playerService.play(youtubeUrl, {
203
+ volume: this.internalState.volume,
204
+ });
205
+ }
206
+ break;
207
+ }
208
+ case 'PREVIOUS': {
209
+ const prevPosition = this.internalState.queuePosition - 1;
210
+ if (prevPosition < 0)
211
+ break;
212
+ if (this.internalState.progress > 3) {
213
+ this.internalState.progress = 0;
214
+ break;
215
+ }
216
+ this.internalState.queuePosition = prevPosition;
217
+ this.internalState.currentTrack =
218
+ this.internalState.queue[prevPosition] ?? null;
219
+ this.internalState.progress = 0;
220
+ break;
221
+ }
222
+ case 'SEEK':
223
+ if (action.position !== undefined) {
224
+ this.internalState.progress = Math.max(0, Math.min(action.position, this.internalState.duration));
225
+ // Note: Seeking via mpv IPC would require additional implementation
226
+ }
227
+ break;
228
+ case 'SET_VOLUME':
229
+ if (action.volume !== undefined) {
230
+ this.internalState.volume = Math.max(0, Math.min(100, action.volume));
231
+ playerService.setVolume(this.internalState.volume);
232
+ }
233
+ break;
234
+ case 'VOLUME_UP':
235
+ this.internalState.volume = Math.min(100, this.internalState.volume + 10);
236
+ playerService.setVolume(this.internalState.volume);
237
+ break;
238
+ case 'VOLUME_DOWN':
239
+ this.internalState.volume = Math.max(0, this.internalState.volume - 10);
240
+ playerService.setVolume(this.internalState.volume);
241
+ break;
242
+ case 'VOLUME_FINE_UP':
243
+ this.internalState.volume = Math.min(100, this.internalState.volume + 1);
244
+ playerService.setVolume(this.internalState.volume);
245
+ break;
246
+ case 'VOLUME_FINE_DOWN':
247
+ this.internalState.volume = Math.max(0, this.internalState.volume - 1);
248
+ playerService.setVolume(this.internalState.volume);
249
+ break;
250
+ case 'TOGGLE_SHUFFLE':
251
+ this.internalState.shuffle = !this.internalState.shuffle;
252
+ break;
253
+ case 'TOGGLE_REPEAT': {
254
+ const repeatModes = ['off', 'all', 'one'];
255
+ const currentIndex = repeatModes.indexOf(this.internalState.repeat);
256
+ this.internalState.repeat =
257
+ repeatModes[(currentIndex + 1) % 3] ?? 'off';
258
+ break;
259
+ }
260
+ case 'SET_QUEUE':
261
+ if (action.queue) {
262
+ this.internalState.queue = action.queue;
263
+ this.internalState.queuePosition = 0;
264
+ }
265
+ break;
266
+ case 'ADD_TO_QUEUE':
267
+ if (action.track) {
268
+ this.internalState.queue = [
269
+ ...this.internalState.queue,
270
+ action.track,
271
+ ];
272
+ }
273
+ break;
274
+ case 'REMOVE_FROM_QUEUE':
275
+ if (action.index !== undefined) {
276
+ const newQueue = [...this.internalState.queue];
277
+ newQueue.splice(action.index, 1);
278
+ this.internalState.queue = newQueue;
279
+ }
280
+ break;
281
+ case 'CLEAR_QUEUE':
282
+ this.internalState.queue = [];
283
+ this.internalState.queuePosition = 0;
284
+ this.internalState.isPlaying = false;
285
+ break;
286
+ case 'SET_QUEUE_POSITION':
287
+ if (action.position >= 0 &&
288
+ action.position < this.internalState.queue.length) {
289
+ this.internalState.queuePosition = action.position;
290
+ this.internalState.currentTrack =
291
+ this.internalState.queue[action.position] ?? null;
292
+ this.internalState.progress = 0;
293
+ }
294
+ break;
295
+ case 'SET_SPEED':
296
+ if (action.speed !== undefined) {
297
+ const clampedSpeed = Math.max(0.25, Math.min(4.0, action.speed));
298
+ this.internalState.speed = clampedSpeed;
299
+ playerService.setSpeed(clampedSpeed);
300
+ }
301
+ break;
302
+ case 'UPDATE_PROGRESS':
303
+ if (action.progress !== undefined) {
304
+ this.internalState.progress = Math.max(0, Math.min(action.progress, this.internalState.duration || action.progress));
305
+ }
306
+ break;
307
+ case 'SET_DURATION':
308
+ if (action.duration !== undefined) {
309
+ this.internalState.duration = action.duration;
310
+ }
311
+ break;
312
+ case 'SET_LOADING':
313
+ if (action.loading !== undefined) {
314
+ this.internalState.isLoading = action.loading;
315
+ }
316
+ break;
317
+ case 'SET_ERROR':
318
+ if (action.error !== undefined) {
319
+ this.internalState.error = action.error;
320
+ this.internalState.isLoading = false;
321
+ }
322
+ break;
323
+ default:
324
+ logger.debug('WebServerManager', 'Unhandled command category', {
325
+ category: action.category,
326
+ });
327
+ }
328
+ // Broadcast updated state after command
329
+ this.broadcastState();
330
+ }
331
+ /**
332
+ * Handle import request from web client
333
+ */
334
+ async handleImportRequest(source, url, name) {
335
+ logger.info('WebServerManager', 'Import request from client', {
336
+ source,
337
+ url,
338
+ name,
339
+ });
340
+ try {
341
+ const importService = getImportService();
342
+ await importService.importPlaylist(source, url, name);
343
+ }
344
+ catch (error) {
345
+ logger.error('WebServerManager', 'Import failed', {
346
+ source,
347
+ url,
348
+ error: error instanceof Error ? error.message : String(error),
349
+ });
350
+ }
351
+ }
352
+ /**
353
+ * Handle search request from web client
354
+ */
355
+ async handleSearchRequest(query, searchType) {
356
+ logger.info('WebServerManager', 'Search request from client', {
357
+ query,
358
+ searchType,
359
+ });
360
+ try {
361
+ const searchService = getSearchService();
362
+ const response = await searchService.search(query, { type: searchType });
363
+ const streamingService = getWebStreamingService();
364
+ streamingService.broadcast({
365
+ type: 'search-results',
366
+ results: response.results,
367
+ });
368
+ }
369
+ catch (error) {
370
+ logger.error('WebServerManager', 'Search failed', {
371
+ query,
372
+ searchType,
373
+ error: error instanceof Error ? error.message : String(error),
374
+ });
375
+ }
376
+ }
377
+ /**
378
+ * Handle config update from web client
379
+ */
380
+ handleConfigUpdate(config) {
381
+ logger.info('WebServerManager', 'Config update from client', { config });
382
+ try {
383
+ const configService = getConfigService();
384
+ // Apply each config key
385
+ for (const [key, value] of Object.entries(config)) {
386
+ configService.set(key, value);
387
+ }
388
+ // Broadcast updated config to all clients
389
+ const streamingService = getWebStreamingService();
390
+ streamingService.broadcast({
391
+ type: 'config-update',
392
+ config,
393
+ });
394
+ }
395
+ catch (error) {
396
+ logger.error('WebServerManager', 'Config update failed', {
397
+ config,
398
+ error: error instanceof Error ? error.message : String(error),
399
+ });
400
+ }
401
+ }
402
+ /**
403
+ * Update player state (call this when player state changes)
404
+ * This is called by PlayerProvider in normal mode to sync state
405
+ */
406
+ updateState(state) {
407
+ if (!this.isRunning)
408
+ return;
409
+ // Update internal state to stay in sync
410
+ this.internalState = { ...state };
411
+ const streamingService = getWebStreamingService();
412
+ streamingService.onStateChange(state);
413
+ }
414
+ /**
415
+ * Broadcast current state to all connected clients
416
+ */
417
+ broadcastState() {
418
+ if (!this.isRunning)
419
+ return;
420
+ const streamingService = getWebStreamingService();
421
+ streamingService.onStateChange(this.internalState);
422
+ }
423
+ /**
424
+ * Get current internal state
425
+ */
426
+ getState() {
427
+ return { ...this.internalState };
428
+ }
429
+ /**
430
+ * Set internal state directly (for sync from external sources)
431
+ */
432
+ setState(state) {
433
+ this.internalState = { ...this.internalState, ...state };
434
+ this.broadcastState();
435
+ }
436
+ /**
437
+ * Set up graceful shutdown hooks
438
+ */
439
+ setupShutdownHooks() {
440
+ const shutdown = async () => {
441
+ await this.stop();
442
+ };
443
+ process.on('beforeExit', shutdown);
444
+ process.on('SIGINT', shutdown);
445
+ process.on('SIGTERM', shutdown);
446
+ this.cleanupHooks.push(() => {
447
+ process.off('beforeExit', shutdown);
448
+ process.off('SIGINT', shutdown);
449
+ process.off('SIGTERM', shutdown);
450
+ });
451
+ }
452
+ /**
453
+ * Check if server is running
454
+ */
455
+ isServerRunning() {
456
+ return this.isRunning;
457
+ }
458
+ /**
459
+ * Get server URL
460
+ */
461
+ getServerUrl() {
462
+ const wsServer = getWebSocketServer();
463
+ return wsServer.getServerUrl();
464
+ }
465
+ /**
466
+ * Get server statistics
467
+ */
468
+ getStats() {
469
+ if (!this.isRunning) {
470
+ return { running: false };
471
+ }
472
+ const streamingService = getWebStreamingService();
473
+ const stats = streamingService.getStats();
474
+ return {
475
+ running: true,
476
+ url: this.getServerUrl(),
477
+ clients: stats.clients,
478
+ };
479
+ }
480
+ /**
481
+ * Update configuration
482
+ */
483
+ updateConfig(config) {
484
+ this.config = { ...this.config, ...config };
485
+ const configService = getConfigService();
486
+ configService.set('webServer', this.config);
487
+ }
488
+ /**
489
+ * Get current configuration
490
+ */
491
+ getConfig() {
492
+ return { ...this.config };
493
+ }
494
+ }
495
+ // Singleton instance
496
+ let webServerManagerInstance = null;
497
+ export function getWebServerManager() {
498
+ if (!webServerManagerInstance) {
499
+ webServerManagerInstance = new WebServerManager();
500
+ }
501
+ return webServerManagerInstance;
502
+ }
@@ -0,0 +1,88 @@
1
+ import type { PlayerState } from '../../types/player.types.ts';
2
+ import type { ServerMessage, ClientMessage } from '../../types/web.types.ts';
3
+ import type { ImportProgress, ImportResult } from '../../types/import.types.ts';
4
+ type MessageHandler = (message: ClientMessage) => void;
5
+ declare class WebStreamingService {
6
+ private clients;
7
+ private clientInfo;
8
+ private messageHandlers;
9
+ private prevState;
10
+ private readonly UPDATE_THROTTLE_MS;
11
+ private lastUpdateTime;
12
+ private pendingUpdate;
13
+ private updateTimer;
14
+ /**
15
+ * Add a client connection
16
+ */
17
+ addClient(clientId: string, ws: WebSocket, authenticated?: boolean): void;
18
+ /**
19
+ * Remove a client connection
20
+ */
21
+ removeClient(clientId: string): void;
22
+ /**
23
+ * Check if a client exists
24
+ */
25
+ hasClient(clientId: string): boolean;
26
+ /**
27
+ * Get client count
28
+ */
29
+ getClientCount(): number;
30
+ /**
31
+ * Get all connected clients
32
+ */
33
+ getClients(): string[];
34
+ /**
35
+ * Handle incoming message from a client
36
+ */
37
+ handleClientMessage(clientId: string, message: unknown): void;
38
+ /**
39
+ * Register a handler for incoming client messages
40
+ */
41
+ onMessage(handler: MessageHandler): () => void;
42
+ /**
43
+ * Send a message to a specific client
44
+ */
45
+ private sendToClient;
46
+ /**
47
+ * Broadcast a message to all connected clients
48
+ */
49
+ broadcast(message: ServerMessage): void;
50
+ /**
51
+ * Compute partial state delta (only changed fields)
52
+ */
53
+ private computeDelta;
54
+ /**
55
+ * Update and broadcast player state (throttled)
56
+ */
57
+ onStateChange(state: PlayerState): void;
58
+ /**
59
+ * Send pending state update to all clients
60
+ */
61
+ private sendPendingUpdate;
62
+ /**
63
+ * Broadcast import progress
64
+ */
65
+ onImportProgress(progress: ImportProgress): void;
66
+ /**
67
+ * Broadcast import result
68
+ */
69
+ onImportResult(result: ImportResult): void;
70
+ /**
71
+ * Broadcast error message
72
+ */
73
+ sendError(error: string, code?: string): void;
74
+ /**
75
+ * Disconnect all clients
76
+ */
77
+ disconnectAll(): void;
78
+ /**
79
+ * Get server statistics
80
+ */
81
+ getStats(): {
82
+ clients: number;
83
+ totalConnections: number;
84
+ uptime: number;
85
+ };
86
+ }
87
+ export declare function getWebStreamingService(): WebStreamingService;
88
+ export {};