@involvex/youtube-music-cli 0.0.47 → 0.0.48
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/CHANGELOG.md +2 -0
- package/dist/cli.js.map +3 -3
- package/dist/youtube-music-cli +0 -0
- package/package.json +1 -1
- package/dist/eslint.config.js +0 -55
- package/dist/package.json +0 -120
- package/dist/scripts/build-cli.js +0 -46
- package/dist/source/app.js +0 -17
- package/dist/source/cli.js +0 -504
- package/dist/source/components/common/ErrorBoundary.js +0 -22
- package/dist/source/components/common/Help.js +0 -18
- package/dist/source/components/common/ShortcutsBar.js +0 -89
- package/dist/source/components/config/ConfigLayout.js +0 -84
- package/dist/source/components/config/KeybindingsLayout.js +0 -107
- package/dist/source/components/export/ExportLayout.js +0 -111
- package/dist/source/components/import/ImportLayout.js +0 -119
- package/dist/source/components/import/ImportProgress.js +0 -73
- package/dist/source/components/layouts/ExploreLayout.js +0 -72
- package/dist/source/components/layouts/HistoryLayout.js +0 -37
- package/dist/source/components/layouts/LyricsLayout.js +0 -89
- package/dist/source/components/layouts/MainLayout.js +0 -190
- package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
- package/dist/source/components/layouts/PlayerLayout.js +0 -9
- package/dist/source/components/layouts/PluginsLayout.js +0 -77
- package/dist/source/components/layouts/SearchLayout.js +0 -193
- package/dist/source/components/layouts/TrendingLayout.js +0 -59
- package/dist/source/components/player/NowPlaying.js +0 -45
- package/dist/source/components/player/PlayerControls.js +0 -83
- package/dist/source/components/player/ProgressBar.js +0 -19
- package/dist/source/components/player/QueueList.js +0 -36
- package/dist/source/components/player/Suggestions.js +0 -50
- package/dist/source/components/playlist/PlaylistList.js +0 -138
- package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
- package/dist/source/components/plugins/PluginsAvailable.js +0 -55
- package/dist/source/components/plugins/PluginsList.js +0 -18
- package/dist/source/components/search/SearchBar.js +0 -55
- package/dist/source/components/search/SearchHistory.js +0 -35
- package/dist/source/components/search/SearchResults.js +0 -280
- package/dist/source/components/settings/Settings.js +0 -211
- package/dist/source/components/theme/ThemeSwitcher.js +0 -11
- package/dist/source/config/themes.config.js +0 -123
- package/dist/source/contexts/theme.context.js +0 -29
- package/dist/source/hooks/useKeyboard.js +0 -188
- package/dist/source/hooks/useKeyboardBlocker.js +0 -45
- package/dist/source/hooks/useNavigation.js +0 -5
- package/dist/source/hooks/usePlayer.js +0 -43
- package/dist/source/hooks/usePlaylist.js +0 -65
- package/dist/source/hooks/useSearch.js +0 -76
- package/dist/source/hooks/useSleepTimer.js +0 -48
- package/dist/source/hooks/useTerminalSize.js +0 -24
- package/dist/source/hooks/useTheme.js +0 -5
- package/dist/source/hooks/useYouTubeMusic.js +0 -112
- package/dist/source/main.js +0 -127
- package/dist/source/services/cache/cache.service.js +0 -67
- package/dist/source/services/completions/completions.service.js +0 -313
- package/dist/source/services/config/config.service.js +0 -191
- package/dist/source/services/discord/discord-rpc.service.js +0 -95
- package/dist/source/services/download/download.service.js +0 -350
- package/dist/source/services/export/export.service.js +0 -131
- package/dist/source/services/history/history.service.js +0 -83
- package/dist/source/services/import/import.service.js +0 -272
- package/dist/source/services/import/spotify.service.js +0 -171
- package/dist/source/services/import/track-matcher.service.js +0 -271
- package/dist/source/services/import/youtube-import.service.js +0 -84
- package/dist/source/services/logger/logger.service.js +0 -52
- package/dist/source/services/lyrics/lyrics.service.js +0 -93
- package/dist/source/services/mpris/mpris.service.js +0 -78
- package/dist/source/services/notification/notification.service.js +0 -57
- package/dist/source/services/player/dependency-check.service.js +0 -140
- package/dist/source/services/player/player.service.js +0 -478
- package/dist/source/services/player-state/player-state.service.js +0 -123
- package/dist/source/services/plugin/plugin-audio-api.js +0 -36
- package/dist/source/services/plugin/plugin-context.js +0 -256
- package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
- package/dist/source/services/plugin/plugin-installer.service.js +0 -248
- package/dist/source/services/plugin/plugin-loader.service.js +0 -161
- package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
- package/dist/source/services/plugin/plugin-registry.service.js +0 -215
- package/dist/source/services/plugin/plugin-ui-api.js +0 -46
- package/dist/source/services/plugin/plugin-updater.service.js +0 -206
- package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
- package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
- package/dist/source/services/version-check/version-check.service.js +0 -121
- package/dist/source/services/web/static-file.service.js +0 -185
- package/dist/source/services/web/web-server-manager.js +0 -507
- package/dist/source/services/web/web-streaming.service.js +0 -292
- package/dist/source/services/web/websocket.server.js +0 -267
- package/dist/source/services/youtube-music/api.js +0 -649
- package/dist/source/services/youtube-music/search.service.js +0 -38
- package/dist/source/stores/history.store.js +0 -64
- package/dist/source/stores/navigation.store.js +0 -90
- package/dist/source/stores/player.store.js +0 -789
- package/dist/source/stores/plugins.store.js +0 -177
- package/dist/source/types/actions.js +0 -1
- package/dist/source/types/cli.types.js +0 -1
- package/dist/source/types/config.types.js +0 -1
- package/dist/source/types/history.types.js +0 -1
- package/dist/source/types/import.types.js +0 -2
- package/dist/source/types/keyboard.types.js +0 -1
- package/dist/source/types/navigation.types.js +0 -1
- package/dist/source/types/player.types.js +0 -1
- package/dist/source/types/playlist.types.js +0 -1
- package/dist/source/types/plugin.types.js +0 -1
- package/dist/source/types/theme.types.js +0 -1
- package/dist/source/types/web.types.js +0 -2
- package/dist/source/types/youtube-music.types.js +0 -1
- package/dist/source/types/youtubei.types.js +0 -3
- package/dist/source/utils/constants.js +0 -135
- package/dist/source/utils/format.js +0 -24
- package/dist/source/utils/icons.js +0 -28
- package/dist/source/utils/search-filters.js +0 -100
|
@@ -1,292 +0,0 @@
|
|
|
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
|
-
'autoplay',
|
|
168
|
-
'isLoading',
|
|
169
|
-
'error',
|
|
170
|
-
];
|
|
171
|
-
for (const field of fields) {
|
|
172
|
-
const prevValue = this.prevState[field];
|
|
173
|
-
const newValue = newState[field];
|
|
174
|
-
// Deep comparison for objects/arrays
|
|
175
|
-
if (typeof prevValue === 'object' && prevValue !== null) {
|
|
176
|
-
if (JSON.stringify(prevValue) !== JSON.stringify(newValue)) {
|
|
177
|
-
delta[field] = newValue;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
else if (prevValue !== newValue) {
|
|
181
|
-
delta[field] = newValue;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return delta;
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Update and broadcast player state (throttled)
|
|
188
|
-
*/
|
|
189
|
-
onStateChange(state) {
|
|
190
|
-
const now = Date.now();
|
|
191
|
-
// Compute delta against OLD prevState BEFORE updating it
|
|
192
|
-
const delta = this.computeDelta(state);
|
|
193
|
-
this.prevState = { ...state };
|
|
194
|
-
// Skip if no changes
|
|
195
|
-
if (Object.keys(delta).length === 0) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
// Merge with pending update
|
|
199
|
-
this.pendingUpdate = { ...(this.pendingUpdate || {}), ...delta };
|
|
200
|
-
// Clear existing timer
|
|
201
|
-
if (this.updateTimer) {
|
|
202
|
-
clearTimeout(this.updateTimer);
|
|
203
|
-
}
|
|
204
|
-
// If throttle period passed, send immediately
|
|
205
|
-
if (now - this.lastUpdateTime >= this.UPDATE_THROTTLE_MS) {
|
|
206
|
-
this.sendPendingUpdate();
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
// Otherwise, schedule update
|
|
210
|
-
this.updateTimer = setTimeout(() => {
|
|
211
|
-
this.sendPendingUpdate();
|
|
212
|
-
}, this.UPDATE_THROTTLE_MS - (now - this.lastUpdateTime));
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Send pending state update to all clients
|
|
217
|
-
*/
|
|
218
|
-
sendPendingUpdate() {
|
|
219
|
-
if (!this.pendingUpdate)
|
|
220
|
-
return;
|
|
221
|
-
this.broadcast({
|
|
222
|
-
type: 'state-update',
|
|
223
|
-
state: this.pendingUpdate,
|
|
224
|
-
});
|
|
225
|
-
this.pendingUpdate = null;
|
|
226
|
-
this.lastUpdateTime = Date.now();
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Broadcast import progress
|
|
230
|
-
*/
|
|
231
|
-
onImportProgress(progress) {
|
|
232
|
-
this.broadcast({
|
|
233
|
-
type: 'import-progress',
|
|
234
|
-
data: progress,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Broadcast import result
|
|
239
|
-
*/
|
|
240
|
-
onImportResult(result) {
|
|
241
|
-
this.broadcast({
|
|
242
|
-
type: 'import-result',
|
|
243
|
-
data: result,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Broadcast error message
|
|
248
|
-
*/
|
|
249
|
-
sendError(error, code) {
|
|
250
|
-
this.broadcast({
|
|
251
|
-
type: 'error',
|
|
252
|
-
error,
|
|
253
|
-
code,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Disconnect all clients
|
|
258
|
-
*/
|
|
259
|
-
disconnectAll() {
|
|
260
|
-
for (const [, ws] of this.clients) {
|
|
261
|
-
try {
|
|
262
|
-
ws.close();
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
// Ignore
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
this.clients.clear();
|
|
269
|
-
this.clientInfo.clear();
|
|
270
|
-
logger.info('WebStreamingService', 'All clients disconnected');
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Get server statistics
|
|
274
|
-
*/
|
|
275
|
-
getStats() {
|
|
276
|
-
const now = Date.now();
|
|
277
|
-
const oldestClient = Array.from(this.clientInfo.values()).sort((a, b) => a.connectedAt - b.connectedAt)[0];
|
|
278
|
-
return {
|
|
279
|
-
clients: this.clients.size,
|
|
280
|
-
totalConnections: this.clientInfo.size,
|
|
281
|
-
uptime: oldestClient ? now - oldestClient.connectedAt : 0,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
// Singleton instance
|
|
286
|
-
let webStreamingServiceInstance = null;
|
|
287
|
-
export function getWebStreamingService() {
|
|
288
|
-
if (!webStreamingServiceInstance) {
|
|
289
|
-
webStreamingServiceInstance = new WebStreamingService();
|
|
290
|
-
}
|
|
291
|
-
return webStreamingServiceInstance;
|
|
292
|
-
}
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
// WebSocket server for web UI
|
|
2
|
-
import { createServer as createHttpServer, } from 'node:http';
|
|
3
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
4
|
-
import { getWebStreamingService } from "./web-streaming.service.js";
|
|
5
|
-
import { getStaticFileService } from "./static-file.service.js";
|
|
6
|
-
import { logger } from "../logger/logger.service.js";
|
|
7
|
-
class WebSocketServerClass {
|
|
8
|
-
httpServer = null;
|
|
9
|
-
wsServer = null;
|
|
10
|
-
config;
|
|
11
|
-
streamingService = getWebStreamingService();
|
|
12
|
-
staticFileService = getStaticFileService();
|
|
13
|
-
onCommand;
|
|
14
|
-
onImportRequest;
|
|
15
|
-
onSearchRequest;
|
|
16
|
-
onConfigUpdate;
|
|
17
|
-
constructor() {
|
|
18
|
-
this.config = {
|
|
19
|
-
enabled: false,
|
|
20
|
-
host: 'localhost',
|
|
21
|
-
port: 8080,
|
|
22
|
-
enableCors: true,
|
|
23
|
-
allowedOrigins: ['*'],
|
|
24
|
-
auth: { enabled: false },
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Start the WebSocket server
|
|
29
|
-
*/
|
|
30
|
-
async start(options) {
|
|
31
|
-
this.config = options.config;
|
|
32
|
-
this.onCommand = options.onCommand;
|
|
33
|
-
this.onImportRequest = options.onImportRequest;
|
|
34
|
-
this.onSearchRequest = options.onSearchRequest;
|
|
35
|
-
this.onConfigUpdate = options.onConfigUpdate;
|
|
36
|
-
logger.info('WebSocketServer', 'Starting server', {
|
|
37
|
-
host: this.config.host,
|
|
38
|
-
port: this.config.port,
|
|
39
|
-
auth: this.config.auth.enabled,
|
|
40
|
-
});
|
|
41
|
-
// Create HTTP server
|
|
42
|
-
this.httpServer = createHttpServer((req, res) => {
|
|
43
|
-
this.handleHttpRequest(req, res);
|
|
44
|
-
});
|
|
45
|
-
// Create WebSocket server
|
|
46
|
-
this.wsServer = new WebSocketServer({
|
|
47
|
-
server: this.httpServer,
|
|
48
|
-
path: '/ws',
|
|
49
|
-
});
|
|
50
|
-
// Handle WebSocket connections
|
|
51
|
-
this.wsServer.on('connection', (ws, req) => {
|
|
52
|
-
this.handleWebSocketConnection(ws, req);
|
|
53
|
-
});
|
|
54
|
-
// Handle HTTP server errors
|
|
55
|
-
this.httpServer.on('error', error => {
|
|
56
|
-
logger.error('WebSocketServer', 'HTTP server error', {
|
|
57
|
-
error: error instanceof Error ? error.message : String(error),
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
// Handle WebSocket server errors
|
|
61
|
-
this.wsServer.on('error', error => {
|
|
62
|
-
logger.error('WebSocketServer', 'WebSocket server error', {
|
|
63
|
-
error: error instanceof Error ? error.message : String(error),
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
// Start listening
|
|
67
|
-
return new Promise((resolve, reject) => {
|
|
68
|
-
this.httpServer.listen({
|
|
69
|
-
host: this.config.host,
|
|
70
|
-
port: this.config.port,
|
|
71
|
-
}, () => {
|
|
72
|
-
logger.info('WebSocketServer', 'Server started', {
|
|
73
|
-
url: `http://${this.config.host}:${this.config.port}`,
|
|
74
|
-
});
|
|
75
|
-
resolve();
|
|
76
|
-
});
|
|
77
|
-
this.httpServer.on('error', reject);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Handle HTTP requests (for static file serving)
|
|
82
|
-
*/
|
|
83
|
-
handleHttpRequest(req, res) {
|
|
84
|
-
// Set CORS headers if enabled
|
|
85
|
-
if (this.config.enableCors) {
|
|
86
|
-
const origin = req.headers.origin;
|
|
87
|
-
if (origin &&
|
|
88
|
-
(this.config.allowedOrigins.includes('*') ||
|
|
89
|
-
this.config.allowedOrigins.includes(origin))) {
|
|
90
|
-
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
91
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
92
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// Handle preflight requests
|
|
96
|
-
if (req.method === 'OPTIONS') {
|
|
97
|
-
res.writeHead(204);
|
|
98
|
-
res.end();
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
// Serve static files
|
|
102
|
-
const url = req.url ?? '/';
|
|
103
|
-
this.staticFileService.serve(url, req, res);
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Handle WebSocket connection
|
|
107
|
-
*/
|
|
108
|
-
handleWebSocketConnection(ws, req) {
|
|
109
|
-
const clientId = this.generateClientId();
|
|
110
|
-
logger.info('WebSocketServer', 'New connection', {
|
|
111
|
-
clientId,
|
|
112
|
-
ip: req.socket.remoteAddress,
|
|
113
|
-
});
|
|
114
|
-
// Handle authentication if enabled
|
|
115
|
-
if (this.config.auth.enabled) {
|
|
116
|
-
const token = this.extractAuthToken(req);
|
|
117
|
-
if (!token || token !== this.config.auth.token) {
|
|
118
|
-
logger.warn('WebSocketServer', 'Authentication failed', { clientId });
|
|
119
|
-
ws.close(1008, 'Authentication failed');
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
// Add client to streaming service
|
|
124
|
-
this.streamingService.addClient(clientId, ws, true);
|
|
125
|
-
// Send welcome message
|
|
126
|
-
this.sendToClient(ws, {
|
|
127
|
-
type: 'auth',
|
|
128
|
-
success: true,
|
|
129
|
-
message: 'Connected to YouTube Music CLI',
|
|
130
|
-
});
|
|
131
|
-
// Handle incoming messages
|
|
132
|
-
ws.on('message', (data) => {
|
|
133
|
-
try {
|
|
134
|
-
const message = JSON.parse(data.toString());
|
|
135
|
-
this.streamingService.handleClientMessage(clientId, message);
|
|
136
|
-
this.handleClientCommand(clientId, message);
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
logger.error('WebSocketServer', 'Failed to parse message', {
|
|
140
|
-
clientId,
|
|
141
|
-
error: error instanceof Error ? error.message : String(error),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
// Handle close
|
|
146
|
-
ws.on('close', () => {
|
|
147
|
-
this.streamingService.removeClient(clientId);
|
|
148
|
-
});
|
|
149
|
-
// Handle errors
|
|
150
|
-
ws.on('error', error => {
|
|
151
|
-
logger.error('WebSocketServer', 'WebSocket error', {
|
|
152
|
-
error: error instanceof Error ? error.message : String(error),
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Handle commands from clients
|
|
158
|
-
*/
|
|
159
|
-
handleClientCommand(_clientId, message) {
|
|
160
|
-
switch (message.type) {
|
|
161
|
-
case 'command':
|
|
162
|
-
if (this.onCommand) {
|
|
163
|
-
this.onCommand(message.action);
|
|
164
|
-
}
|
|
165
|
-
break;
|
|
166
|
-
case 'import-request':
|
|
167
|
-
if (this.onImportRequest) {
|
|
168
|
-
this.onImportRequest(message.source, message.url, message.name);
|
|
169
|
-
}
|
|
170
|
-
break;
|
|
171
|
-
case 'search-request':
|
|
172
|
-
if (this.onSearchRequest) {
|
|
173
|
-
this.onSearchRequest(message.query, message.searchType);
|
|
174
|
-
}
|
|
175
|
-
break;
|
|
176
|
-
case 'config-update':
|
|
177
|
-
if (this.onConfigUpdate) {
|
|
178
|
-
this.onConfigUpdate(message.config);
|
|
179
|
-
}
|
|
180
|
-
break;
|
|
181
|
-
case 'auth-request':
|
|
182
|
-
// Already handled in connection phase
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Send message to a specific WebSocket client
|
|
188
|
-
*/
|
|
189
|
-
sendToClient(ws, message) {
|
|
190
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
191
|
-
try {
|
|
192
|
-
ws.send(JSON.stringify(message));
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
logger.error('WebSocketServer', 'Failed to send message', {
|
|
196
|
-
error: error instanceof Error ? error.message : String(error),
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Generate a unique client ID
|
|
203
|
-
*/
|
|
204
|
-
generateClientId() {
|
|
205
|
-
return `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Extract auth token from request
|
|
209
|
-
*/
|
|
210
|
-
extractAuthToken(req) {
|
|
211
|
-
// Check Authorization header
|
|
212
|
-
const authHeader = req.headers.authorization;
|
|
213
|
-
if (authHeader?.startsWith('Bearer ')) {
|
|
214
|
-
return authHeader.substring(7);
|
|
215
|
-
}
|
|
216
|
-
// Check query parameter
|
|
217
|
-
const url = req.url ?? '';
|
|
218
|
-
const urlObj = new URL(url, `http://${req.headers.host}`);
|
|
219
|
-
return urlObj.searchParams.get('token');
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Stop the server
|
|
223
|
-
*/
|
|
224
|
-
async stop() {
|
|
225
|
-
logger.info('WebSocketServer', 'Stopping server');
|
|
226
|
-
// Disconnect all clients
|
|
227
|
-
this.streamingService.disconnectAll();
|
|
228
|
-
// Close WebSocket server
|
|
229
|
-
if (this.wsServer) {
|
|
230
|
-
this.wsServer.close();
|
|
231
|
-
this.wsServer = null;
|
|
232
|
-
}
|
|
233
|
-
// Close HTTP server
|
|
234
|
-
if (this.httpServer) {
|
|
235
|
-
return new Promise(resolve => {
|
|
236
|
-
this.httpServer.close(() => {
|
|
237
|
-
this.httpServer = null;
|
|
238
|
-
logger.info('WebSocketServer', 'Server stopped');
|
|
239
|
-
resolve();
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Check if server is running
|
|
246
|
-
*/
|
|
247
|
-
isRunning() {
|
|
248
|
-
return this.httpServer !== null;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Get server URL
|
|
252
|
-
*/
|
|
253
|
-
getServerUrl() {
|
|
254
|
-
if (!this.isRunning()) {
|
|
255
|
-
throw new Error('Server is not running');
|
|
256
|
-
}
|
|
257
|
-
return `http://${this.config.host}:${this.config.port}`;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
// Singleton instance
|
|
261
|
-
let webSocketServerInstance = null;
|
|
262
|
-
export function getWebSocketServer() {
|
|
263
|
-
if (!webSocketServerInstance) {
|
|
264
|
-
webSocketServerInstance = new WebSocketServerClass();
|
|
265
|
-
}
|
|
266
|
-
return webSocketServerInstance;
|
|
267
|
-
}
|