@involvex/youtube-music-cli 0.0.17 → 0.0.19

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 (37) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/source/cli.js +123 -8
  3. package/dist/source/components/common/ShortcutsBar.js +2 -2
  4. package/dist/source/components/import/ImportLayout.d.ts +1 -0
  5. package/dist/source/components/import/ImportLayout.js +119 -0
  6. package/dist/source/components/import/ImportProgress.d.ts +6 -0
  7. package/dist/source/components/import/ImportProgress.js +73 -0
  8. package/dist/source/components/layouts/MainLayout.js +7 -0
  9. package/dist/source/components/settings/Settings.js +6 -2
  10. package/dist/source/services/config/config.service.js +10 -0
  11. package/dist/source/services/import/import.service.d.ts +44 -0
  12. package/dist/source/services/import/import.service.js +272 -0
  13. package/dist/source/services/import/spotify.service.d.ts +40 -0
  14. package/dist/source/services/import/spotify.service.js +171 -0
  15. package/dist/source/services/import/track-matcher.service.d.ts +60 -0
  16. package/dist/source/services/import/track-matcher.service.js +271 -0
  17. package/dist/source/services/import/youtube-import.service.d.ts +17 -0
  18. package/dist/source/services/import/youtube-import.service.js +84 -0
  19. package/dist/source/services/web/static-file.service.d.ts +31 -0
  20. package/dist/source/services/web/static-file.service.js +174 -0
  21. package/dist/source/services/web/web-server-manager.d.ts +66 -0
  22. package/dist/source/services/web/web-server-manager.js +219 -0
  23. package/dist/source/services/web/web-streaming.service.d.ts +88 -0
  24. package/dist/source/services/web/web-streaming.service.js +290 -0
  25. package/dist/source/services/web/websocket.server.d.ts +58 -0
  26. package/dist/source/services/web/websocket.server.js +253 -0
  27. package/dist/source/stores/player.store.js +27 -0
  28. package/dist/source/types/cli.types.d.ts +8 -0
  29. package/dist/source/types/config.types.d.ts +2 -0
  30. package/dist/source/types/import.types.d.ts +72 -0
  31. package/dist/source/types/import.types.js +2 -0
  32. package/dist/source/types/web.types.d.ts +89 -0
  33. package/dist/source/types/web.types.js +2 -0
  34. package/dist/source/utils/constants.d.ts +1 -0
  35. package/dist/source/utils/constants.js +1 -0
  36. package/dist/youtube-music-cli.exe +0 -0
  37. package/package.json +8 -3
@@ -0,0 +1,219 @@
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 { logger } from "../logger/logger.service.js";
6
+ class WebServerManager {
7
+ config;
8
+ isRunning = false;
9
+ cleanupHooks = [];
10
+ constructor() {
11
+ // Load config or use defaults
12
+ const configService = getConfigService();
13
+ const savedConfig = configService.get('webServer');
14
+ this.config = savedConfig ?? {
15
+ enabled: false,
16
+ host: 'localhost',
17
+ port: 8080,
18
+ enableCors: true,
19
+ allowedOrigins: ['*'],
20
+ auth: { enabled: false },
21
+ };
22
+ // Save default config if not present
23
+ if (!savedConfig) {
24
+ configService.set('webServer', this.config);
25
+ }
26
+ }
27
+ /**
28
+ * Start the web server
29
+ */
30
+ async start(options) {
31
+ if (this.isRunning) {
32
+ logger.warn('WebServerManager', 'Server already running');
33
+ return;
34
+ }
35
+ // Apply CLI options
36
+ const finalConfig = { ...this.config };
37
+ if (options) {
38
+ if (options.host !== undefined) {
39
+ finalConfig.host = options.host;
40
+ }
41
+ if (options.port !== undefined) {
42
+ finalConfig.port = options.port;
43
+ }
44
+ if (options.auth !== undefined) {
45
+ finalConfig.auth.enabled = true;
46
+ finalConfig.auth.token = options.auth;
47
+ }
48
+ }
49
+ logger.info('WebServerManager', 'Starting web server', finalConfig);
50
+ try {
51
+ const wsServer = getWebSocketServer();
52
+ // Set up command handler
53
+ const cleanupCommand = this.setupCommandHandler();
54
+ this.cleanupHooks.push(cleanupCommand);
55
+ // Set up import handler
56
+ const cleanupImport = this.setupImportHandler();
57
+ this.cleanupHooks.push(cleanupImport);
58
+ // Start the server
59
+ await wsServer.start({
60
+ config: finalConfig,
61
+ onCommand: this.handleCommand.bind(this),
62
+ onImportRequest: this.handleImportRequest.bind(this),
63
+ });
64
+ this.isRunning = true;
65
+ // Set up graceful shutdown
66
+ this.setupShutdownHooks();
67
+ }
68
+ catch (error) {
69
+ logger.error('WebServerManager', 'Failed to start server', {
70
+ error: error instanceof Error ? error.message : String(error),
71
+ });
72
+ throw error;
73
+ }
74
+ }
75
+ /**
76
+ * Stop the web server
77
+ */
78
+ async stop() {
79
+ if (!this.isRunning) {
80
+ return;
81
+ }
82
+ logger.info('WebServerManager', 'Stopping web server');
83
+ // Clean up hooks
84
+ for (const cleanup of this.cleanupHooks) {
85
+ cleanup();
86
+ }
87
+ this.cleanupHooks = [];
88
+ // Stop the WebSocket server
89
+ const wsServer = getWebSocketServer();
90
+ await wsServer.stop();
91
+ this.isRunning = false;
92
+ }
93
+ /**
94
+ * Set up player command handler
95
+ */
96
+ setupCommandHandler() {
97
+ const streamingService = getWebStreamingService();
98
+ const unsubscribe = streamingService.onMessage(message => {
99
+ if (message.type === 'command') {
100
+ this.handleCommand(message.action);
101
+ }
102
+ });
103
+ return unsubscribe;
104
+ }
105
+ /**
106
+ * Set up import progress handler
107
+ */
108
+ setupImportHandler() {
109
+ const importService = getImportService();
110
+ const streamingService = getWebStreamingService();
111
+ const unsubscribe = importService.onProgress(progress => {
112
+ streamingService.onImportProgress(progress);
113
+ });
114
+ return unsubscribe;
115
+ }
116
+ /**
117
+ * Handle command from web client
118
+ */
119
+ handleCommand(action) {
120
+ // This will be handled by the player store
121
+ logger.debug('WebServerManager', 'Received command from client', { action });
122
+ }
123
+ /**
124
+ * Handle import request from web client
125
+ */
126
+ async handleImportRequest(source, url, name) {
127
+ logger.info('WebServerManager', 'Import request from client', {
128
+ source,
129
+ url,
130
+ name,
131
+ });
132
+ try {
133
+ const importService = getImportService();
134
+ await importService.importPlaylist(source, url, name);
135
+ }
136
+ catch (error) {
137
+ logger.error('WebServerManager', 'Import failed', {
138
+ source,
139
+ url,
140
+ error: error instanceof Error ? error.message : String(error),
141
+ });
142
+ }
143
+ }
144
+ /**
145
+ * Update player state (call this when player state changes)
146
+ */
147
+ updateState(state) {
148
+ if (!this.isRunning)
149
+ return;
150
+ const streamingService = getWebStreamingService();
151
+ streamingService.onStateChange(state);
152
+ }
153
+ /**
154
+ * Set up graceful shutdown hooks
155
+ */
156
+ setupShutdownHooks() {
157
+ const shutdown = async () => {
158
+ await this.stop();
159
+ };
160
+ process.on('beforeExit', shutdown);
161
+ process.on('SIGINT', shutdown);
162
+ process.on('SIGTERM', shutdown);
163
+ this.cleanupHooks.push(() => {
164
+ process.off('beforeExit', shutdown);
165
+ process.off('SIGINT', shutdown);
166
+ process.off('SIGTERM', shutdown);
167
+ });
168
+ }
169
+ /**
170
+ * Check if server is running
171
+ */
172
+ isServerRunning() {
173
+ return this.isRunning;
174
+ }
175
+ /**
176
+ * Get server URL
177
+ */
178
+ getServerUrl() {
179
+ const wsServer = getWebSocketServer();
180
+ return wsServer.getServerUrl();
181
+ }
182
+ /**
183
+ * Get server statistics
184
+ */
185
+ getStats() {
186
+ if (!this.isRunning) {
187
+ return { running: false };
188
+ }
189
+ const streamingService = getWebStreamingService();
190
+ const stats = streamingService.getStats();
191
+ return {
192
+ running: true,
193
+ url: this.getServerUrl(),
194
+ clients: stats.clients,
195
+ };
196
+ }
197
+ /**
198
+ * Update configuration
199
+ */
200
+ updateConfig(config) {
201
+ this.config = { ...this.config, ...config };
202
+ const configService = getConfigService();
203
+ configService.set('webServer', this.config);
204
+ }
205
+ /**
206
+ * Get current configuration
207
+ */
208
+ getConfig() {
209
+ return { ...this.config };
210
+ }
211
+ }
212
+ // Singleton instance
213
+ let webServerManagerInstance = null;
214
+ export function getWebServerManager() {
215
+ if (!webServerManagerInstance) {
216
+ webServerManagerInstance = new WebServerManager();
217
+ }
218
+ return webServerManagerInstance;
219
+ }
@@ -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 {};
@@ -0,0 +1,290 @@
1
+ import { logger } from "../logger/logger.service.js";
2
+ class WebStreamingService {
3
+ clients = new Map();
4
+ clientInfo = new Map();
5
+ messageHandlers = new Set();
6
+ prevState = null;
7
+ UPDATE_THROTTLE_MS = 250; // Throttle updates to 4/sec
8
+ lastUpdateTime = 0;
9
+ pendingUpdate = null;
10
+ updateTimer = null;
11
+ /**
12
+ * Add a client connection
13
+ */
14
+ addClient(clientId, ws, authenticated = true) {
15
+ this.clients.set(clientId, ws);
16
+ this.clientInfo.set(clientId, {
17
+ id: clientId,
18
+ authenticated,
19
+ connectedAt: Date.now(),
20
+ lastHeartbeat: Date.now(),
21
+ });
22
+ logger.info('WebStreamingService', 'Client connected', {
23
+ clientId,
24
+ authenticated,
25
+ });
26
+ // Send initial state if available
27
+ if (this.prevState) {
28
+ this.sendToClient(clientId, {
29
+ type: 'state-update',
30
+ state: this.prevState,
31
+ });
32
+ }
33
+ // Send connection event
34
+ this.broadcast({
35
+ type: 'event',
36
+ event: 'client-connected',
37
+ data: { clientId, clientCount: this.clients.size },
38
+ });
39
+ }
40
+ /**
41
+ * Remove a client connection
42
+ */
43
+ removeClient(clientId) {
44
+ const hadClient = this.clients.has(clientId);
45
+ this.clients.delete(clientId);
46
+ this.clientInfo.delete(clientId);
47
+ if (hadClient) {
48
+ logger.info('WebStreamingService', 'Client disconnected', {
49
+ clientId,
50
+ remainingClients: this.clients.size,
51
+ });
52
+ this.broadcast({
53
+ type: 'event',
54
+ event: 'client-disconnected',
55
+ data: { clientId, clientCount: this.clients.size },
56
+ });
57
+ }
58
+ }
59
+ /**
60
+ * Check if a client exists
61
+ */
62
+ hasClient(clientId) {
63
+ return this.clients.has(clientId);
64
+ }
65
+ /**
66
+ * Get client count
67
+ */
68
+ getClientCount() {
69
+ return this.clients.size;
70
+ }
71
+ /**
72
+ * Get all connected clients
73
+ */
74
+ getClients() {
75
+ return Array.from(this.clients.keys());
76
+ }
77
+ /**
78
+ * Handle incoming message from a client
79
+ */
80
+ handleClientMessage(clientId, message) {
81
+ try {
82
+ const msg = message;
83
+ // Update client heartbeat
84
+ const info = this.clientInfo.get(clientId);
85
+ if (info) {
86
+ info.lastHeartbeat = Date.now();
87
+ this.clientInfo.set(clientId, info);
88
+ }
89
+ // Emit to all handlers
90
+ for (const handler of this.messageHandlers) {
91
+ handler(msg);
92
+ }
93
+ }
94
+ catch (error) {
95
+ logger.error('WebStreamingService', 'Failed to handle client message', {
96
+ clientId,
97
+ error: error instanceof Error ? error.message : String(error),
98
+ });
99
+ }
100
+ }
101
+ /**
102
+ * Register a handler for incoming client messages
103
+ */
104
+ onMessage(handler) {
105
+ this.messageHandlers.add(handler);
106
+ return () => {
107
+ this.messageHandlers.delete(handler);
108
+ };
109
+ }
110
+ /**
111
+ * Send a message to a specific client
112
+ */
113
+ sendToClient(clientId, message) {
114
+ const ws = this.clients.get(clientId);
115
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
116
+ return;
117
+ }
118
+ try {
119
+ ws.send(JSON.stringify(message));
120
+ }
121
+ catch (error) {
122
+ logger.error('WebStreamingService', 'Failed to send to client', {
123
+ clientId,
124
+ error: error instanceof Error ? error.message : String(error),
125
+ });
126
+ }
127
+ }
128
+ /**
129
+ * Broadcast a message to all connected clients
130
+ */
131
+ broadcast(message) {
132
+ const data = JSON.stringify(message);
133
+ for (const [clientId, ws] of this.clients) {
134
+ if (ws.readyState === WebSocket.OPEN) {
135
+ try {
136
+ ws.send(data);
137
+ }
138
+ catch (error) {
139
+ logger.error('WebStreamingService', 'Failed to broadcast to client', {
140
+ clientId,
141
+ error: error instanceof Error ? error.message : String(error),
142
+ });
143
+ }
144
+ }
145
+ }
146
+ }
147
+ /**
148
+ * Compute partial state delta (only changed fields)
149
+ */
150
+ computeDelta(newState) {
151
+ if (!this.prevState) {
152
+ return newState;
153
+ }
154
+ const delta = {};
155
+ // Check each field for changes
156
+ const fields = [
157
+ 'currentTrack',
158
+ 'isPlaying',
159
+ 'volume',
160
+ 'speed',
161
+ 'progress',
162
+ 'duration',
163
+ 'queue',
164
+ 'queuePosition',
165
+ 'repeat',
166
+ 'shuffle',
167
+ 'isLoading',
168
+ 'error',
169
+ ];
170
+ for (const field of fields) {
171
+ const prevValue = this.prevState[field];
172
+ const newValue = newState[field];
173
+ // Deep comparison for objects/arrays
174
+ if (typeof prevValue === 'object' && prevValue !== null) {
175
+ if (JSON.stringify(prevValue) !== JSON.stringify(newValue)) {
176
+ delta[field] = newValue;
177
+ }
178
+ }
179
+ else if (prevValue !== newValue) {
180
+ delta[field] = newValue;
181
+ }
182
+ }
183
+ return delta;
184
+ }
185
+ /**
186
+ * Update and broadcast player state (throttled)
187
+ */
188
+ onStateChange(state) {
189
+ this.prevState = { ...state };
190
+ const now = Date.now();
191
+ const delta = this.computeDelta(state);
192
+ // Skip if no changes
193
+ if (Object.keys(delta).length === 0) {
194
+ return;
195
+ }
196
+ // Merge with pending update
197
+ this.pendingUpdate = { ...(this.pendingUpdate || {}), ...delta };
198
+ // Clear existing timer
199
+ if (this.updateTimer) {
200
+ clearTimeout(this.updateTimer);
201
+ }
202
+ // If throttle period passed, send immediately
203
+ if (now - this.lastUpdateTime >= this.UPDATE_THROTTLE_MS) {
204
+ this.sendPendingUpdate();
205
+ }
206
+ else {
207
+ // Otherwise, schedule update
208
+ this.updateTimer = setTimeout(() => {
209
+ this.sendPendingUpdate();
210
+ }, this.UPDATE_THROTTLE_MS - (now - this.lastUpdateTime));
211
+ }
212
+ }
213
+ /**
214
+ * Send pending state update to all clients
215
+ */
216
+ sendPendingUpdate() {
217
+ if (!this.pendingUpdate)
218
+ return;
219
+ this.broadcast({
220
+ type: 'state-update',
221
+ state: this.pendingUpdate,
222
+ });
223
+ this.pendingUpdate = null;
224
+ this.lastUpdateTime = Date.now();
225
+ }
226
+ /**
227
+ * Broadcast import progress
228
+ */
229
+ onImportProgress(progress) {
230
+ this.broadcast({
231
+ type: 'import-progress',
232
+ data: progress,
233
+ });
234
+ }
235
+ /**
236
+ * Broadcast import result
237
+ */
238
+ onImportResult(result) {
239
+ this.broadcast({
240
+ type: 'import-result',
241
+ data: result,
242
+ });
243
+ }
244
+ /**
245
+ * Broadcast error message
246
+ */
247
+ sendError(error, code) {
248
+ this.broadcast({
249
+ type: 'error',
250
+ error,
251
+ code,
252
+ });
253
+ }
254
+ /**
255
+ * Disconnect all clients
256
+ */
257
+ disconnectAll() {
258
+ for (const [, ws] of this.clients) {
259
+ try {
260
+ ws.close();
261
+ }
262
+ catch {
263
+ // Ignore
264
+ }
265
+ }
266
+ this.clients.clear();
267
+ this.clientInfo.clear();
268
+ logger.info('WebStreamingService', 'All clients disconnected');
269
+ }
270
+ /**
271
+ * Get server statistics
272
+ */
273
+ getStats() {
274
+ const now = Date.now();
275
+ const oldestClient = Array.from(this.clientInfo.values()).sort((a, b) => a.connectedAt - b.connectedAt)[0];
276
+ return {
277
+ clients: this.clients.size,
278
+ totalConnections: this.clientInfo.size,
279
+ uptime: oldestClient ? now - oldestClient.connectedAt : 0,
280
+ };
281
+ }
282
+ }
283
+ // Singleton instance
284
+ let webStreamingServiceInstance = null;
285
+ export function getWebStreamingService() {
286
+ if (!webStreamingServiceInstance) {
287
+ webStreamingServiceInstance = new WebStreamingService();
288
+ }
289
+ return webStreamingServiceInstance;
290
+ }
@@ -0,0 +1,58 @@
1
+ import type { WebServerConfig } from '../../types/web.types.ts';
2
+ interface WebSocketServerOptions {
3
+ config: WebServerConfig;
4
+ onCommand?: (action: unknown) => void;
5
+ onImportRequest?: (source: 'spotify' | 'youtube', url: string, name?: string) => void;
6
+ }
7
+ declare class WebSocketServerClass {
8
+ private httpServer;
9
+ private wsServer;
10
+ private config;
11
+ private streamingService;
12
+ private staticFileService;
13
+ private onCommand?;
14
+ private onImportRequest?;
15
+ constructor();
16
+ /**
17
+ * Start the WebSocket server
18
+ */
19
+ start(options: WebSocketServerOptions): Promise<void>;
20
+ /**
21
+ * Handle HTTP requests (for static file serving)
22
+ */
23
+ private handleHttpRequest;
24
+ /**
25
+ * Handle WebSocket connection
26
+ */
27
+ private handleWebSocketConnection;
28
+ /**
29
+ * Handle commands from clients
30
+ */
31
+ private handleClientCommand;
32
+ /**
33
+ * Send message to a specific WebSocket client
34
+ */
35
+ private sendToClient;
36
+ /**
37
+ * Generate a unique client ID
38
+ */
39
+ private generateClientId;
40
+ /**
41
+ * Extract auth token from request
42
+ */
43
+ private extractAuthToken;
44
+ /**
45
+ * Stop the server
46
+ */
47
+ stop(): Promise<void>;
48
+ /**
49
+ * Check if server is running
50
+ */
51
+ isRunning(): boolean;
52
+ /**
53
+ * Get server URL
54
+ */
55
+ getServerUrl(): string;
56
+ }
57
+ export declare function getWebSocketServer(): WebSocketServerClass;
58
+ export {};