@involvex/youtube-music-cli 0.0.18 → 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.
- package/CHANGELOG.md +2 -0
- package/dist/source/cli.js +123 -8
- package/dist/source/components/import/ImportLayout.d.ts +1 -0
- package/dist/source/components/import/ImportLayout.js +119 -0
- package/dist/source/components/import/ImportProgress.d.ts +6 -0
- package/dist/source/components/import/ImportProgress.js +73 -0
- package/dist/source/components/layouts/MainLayout.js +7 -0
- package/dist/source/components/settings/Settings.js +6 -2
- package/dist/source/services/config/config.service.js +10 -0
- package/dist/source/services/import/import.service.d.ts +44 -0
- package/dist/source/services/import/import.service.js +272 -0
- package/dist/source/services/import/spotify.service.d.ts +40 -0
- package/dist/source/services/import/spotify.service.js +171 -0
- package/dist/source/services/import/track-matcher.service.d.ts +60 -0
- package/dist/source/services/import/track-matcher.service.js +271 -0
- package/dist/source/services/import/youtube-import.service.d.ts +17 -0
- package/dist/source/services/import/youtube-import.service.js +84 -0
- package/dist/source/services/web/static-file.service.d.ts +31 -0
- package/dist/source/services/web/static-file.service.js +174 -0
- package/dist/source/services/web/web-server-manager.d.ts +66 -0
- package/dist/source/services/web/web-server-manager.js +219 -0
- package/dist/source/services/web/web-streaming.service.d.ts +88 -0
- package/dist/source/services/web/web-streaming.service.js +290 -0
- package/dist/source/services/web/websocket.server.d.ts +58 -0
- package/dist/source/services/web/websocket.server.js +253 -0
- package/dist/source/stores/player.store.js +24 -0
- package/dist/source/types/cli.types.d.ts +8 -0
- package/dist/source/types/config.types.d.ts +2 -0
- package/dist/source/types/import.types.d.ts +72 -0
- package/dist/source/types/import.types.js +2 -0
- package/dist/source/types/web.types.d.ts +89 -0
- package/dist/source/types/web.types.js +2 -0
- package/dist/source/utils/constants.d.ts +1 -0
- package/dist/source/utils/constants.js +1 -0
- package/dist/youtube-music-cli.exe +0 -0
- package/package.json +8 -3
|
@@ -0,0 +1,253 @@
|
|
|
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
|
+
constructor() {
|
|
16
|
+
this.config = {
|
|
17
|
+
enabled: false,
|
|
18
|
+
host: 'localhost',
|
|
19
|
+
port: 8080,
|
|
20
|
+
enableCors: true,
|
|
21
|
+
allowedOrigins: ['*'],
|
|
22
|
+
auth: { enabled: false },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Start the WebSocket server
|
|
27
|
+
*/
|
|
28
|
+
async start(options) {
|
|
29
|
+
this.config = options.config;
|
|
30
|
+
this.onCommand = options.onCommand;
|
|
31
|
+
this.onImportRequest = options.onImportRequest;
|
|
32
|
+
logger.info('WebSocketServer', 'Starting server', {
|
|
33
|
+
host: this.config.host,
|
|
34
|
+
port: this.config.port,
|
|
35
|
+
auth: this.config.auth.enabled,
|
|
36
|
+
});
|
|
37
|
+
// Create HTTP server
|
|
38
|
+
this.httpServer = createHttpServer((req, res) => {
|
|
39
|
+
this.handleHttpRequest(req, res);
|
|
40
|
+
});
|
|
41
|
+
// Create WebSocket server
|
|
42
|
+
this.wsServer = new WebSocketServer({
|
|
43
|
+
server: this.httpServer,
|
|
44
|
+
path: '/ws',
|
|
45
|
+
});
|
|
46
|
+
// Handle WebSocket connections
|
|
47
|
+
this.wsServer.on('connection', (ws, req) => {
|
|
48
|
+
this.handleWebSocketConnection(ws, req);
|
|
49
|
+
});
|
|
50
|
+
// Handle HTTP server errors
|
|
51
|
+
this.httpServer.on('error', error => {
|
|
52
|
+
logger.error('WebSocketServer', 'HTTP server error', {
|
|
53
|
+
error: error instanceof Error ? error.message : String(error),
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
// Handle WebSocket server errors
|
|
57
|
+
this.wsServer.on('error', error => {
|
|
58
|
+
logger.error('WebSocketServer', 'WebSocket server error', {
|
|
59
|
+
error: error instanceof Error ? error.message : String(error),
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
// Start listening
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
this.httpServer.listen({
|
|
65
|
+
host: this.config.host,
|
|
66
|
+
port: this.config.port,
|
|
67
|
+
}, () => {
|
|
68
|
+
logger.info('WebSocketServer', 'Server started', {
|
|
69
|
+
url: `http://${this.config.host}:${this.config.port}`,
|
|
70
|
+
});
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
this.httpServer.on('error', reject);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Handle HTTP requests (for static file serving)
|
|
78
|
+
*/
|
|
79
|
+
handleHttpRequest(req, res) {
|
|
80
|
+
// Set CORS headers if enabled
|
|
81
|
+
if (this.config.enableCors) {
|
|
82
|
+
const origin = req.headers.origin;
|
|
83
|
+
if (origin &&
|
|
84
|
+
(this.config.allowedOrigins.includes('*') ||
|
|
85
|
+
this.config.allowedOrigins.includes(origin))) {
|
|
86
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
87
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
88
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Handle preflight requests
|
|
92
|
+
if (req.method === 'OPTIONS') {
|
|
93
|
+
res.writeHead(204);
|
|
94
|
+
res.end();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Serve static files
|
|
98
|
+
const url = req.url ?? '/';
|
|
99
|
+
this.staticFileService.serve(url, req, res);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Handle WebSocket connection
|
|
103
|
+
*/
|
|
104
|
+
handleWebSocketConnection(ws, req) {
|
|
105
|
+
const clientId = this.generateClientId();
|
|
106
|
+
logger.info('WebSocketServer', 'New connection', {
|
|
107
|
+
clientId,
|
|
108
|
+
ip: req.socket.remoteAddress,
|
|
109
|
+
});
|
|
110
|
+
// Handle authentication if enabled
|
|
111
|
+
if (this.config.auth.enabled) {
|
|
112
|
+
const token = this.extractAuthToken(req);
|
|
113
|
+
if (!token || token !== this.config.auth.token) {
|
|
114
|
+
logger.warn('WebSocketServer', 'Authentication failed', { clientId });
|
|
115
|
+
ws.close(1008, 'Authentication failed');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Add client to streaming service
|
|
120
|
+
this.streamingService.addClient(clientId, ws, true);
|
|
121
|
+
// Send welcome message
|
|
122
|
+
this.sendToClient(ws, {
|
|
123
|
+
type: 'auth',
|
|
124
|
+
success: true,
|
|
125
|
+
message: 'Connected to YouTube Music CLI',
|
|
126
|
+
});
|
|
127
|
+
// Handle incoming messages
|
|
128
|
+
ws.on('message', (data) => {
|
|
129
|
+
try {
|
|
130
|
+
const message = JSON.parse(data.toString());
|
|
131
|
+
this.streamingService.handleClientMessage(clientId, message);
|
|
132
|
+
this.handleClientCommand(clientId, message);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
logger.error('WebSocketServer', 'Failed to parse message', {
|
|
136
|
+
clientId,
|
|
137
|
+
error: error instanceof Error ? error.message : String(error),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// Handle close
|
|
142
|
+
ws.on('close', () => {
|
|
143
|
+
this.streamingService.removeClient(clientId);
|
|
144
|
+
});
|
|
145
|
+
// Handle errors
|
|
146
|
+
ws.on('error', error => {
|
|
147
|
+
logger.error('WebSocketServer', 'WebSocket error', {
|
|
148
|
+
error: error instanceof Error ? error.message : String(error),
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Handle commands from clients
|
|
154
|
+
*/
|
|
155
|
+
handleClientCommand(_clientId, message) {
|
|
156
|
+
switch (message.type) {
|
|
157
|
+
case 'command':
|
|
158
|
+
if (this.onCommand) {
|
|
159
|
+
this.onCommand(message.action);
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case 'import-request':
|
|
163
|
+
if (this.onImportRequest) {
|
|
164
|
+
this.onImportRequest(message.source, message.url, message.name);
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
case 'auth-request':
|
|
168
|
+
// Already handled in connection phase
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Send message to a specific WebSocket client
|
|
174
|
+
*/
|
|
175
|
+
sendToClient(ws, message) {
|
|
176
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
177
|
+
try {
|
|
178
|
+
ws.send(JSON.stringify(message));
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
logger.error('WebSocketServer', 'Failed to send message', {
|
|
182
|
+
error: error instanceof Error ? error.message : String(error),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Generate a unique client ID
|
|
189
|
+
*/
|
|
190
|
+
generateClientId() {
|
|
191
|
+
return `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Extract auth token from request
|
|
195
|
+
*/
|
|
196
|
+
extractAuthToken(req) {
|
|
197
|
+
// Check Authorization header
|
|
198
|
+
const authHeader = req.headers.authorization;
|
|
199
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
200
|
+
return authHeader.substring(7);
|
|
201
|
+
}
|
|
202
|
+
// Check query parameter
|
|
203
|
+
const url = req.url ?? '';
|
|
204
|
+
const urlObj = new URL(url, `http://${req.headers.host}`);
|
|
205
|
+
return urlObj.searchParams.get('token');
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Stop the server
|
|
209
|
+
*/
|
|
210
|
+
async stop() {
|
|
211
|
+
logger.info('WebSocketServer', 'Stopping server');
|
|
212
|
+
// Disconnect all clients
|
|
213
|
+
this.streamingService.disconnectAll();
|
|
214
|
+
// Close WebSocket server
|
|
215
|
+
if (this.wsServer) {
|
|
216
|
+
this.wsServer.close();
|
|
217
|
+
this.wsServer = null;
|
|
218
|
+
}
|
|
219
|
+
// Close HTTP server
|
|
220
|
+
if (this.httpServer) {
|
|
221
|
+
return new Promise(resolve => {
|
|
222
|
+
this.httpServer.close(() => {
|
|
223
|
+
this.httpServer = null;
|
|
224
|
+
logger.info('WebSocketServer', 'Server stopped');
|
|
225
|
+
resolve();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Check if server is running
|
|
232
|
+
*/
|
|
233
|
+
isRunning() {
|
|
234
|
+
return this.httpServer !== null;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get server URL
|
|
238
|
+
*/
|
|
239
|
+
getServerUrl() {
|
|
240
|
+
if (!this.isRunning()) {
|
|
241
|
+
throw new Error('Server is not running');
|
|
242
|
+
}
|
|
243
|
+
return `http://${this.config.host}:${this.config.port}`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Singleton instance
|
|
247
|
+
let webSocketServerInstance = null;
|
|
248
|
+
export function getWebSocketServer() {
|
|
249
|
+
if (!webSocketServerInstance) {
|
|
250
|
+
webSocketServerInstance = new WebSocketServerClass();
|
|
251
|
+
}
|
|
252
|
+
return webSocketServerInstance;
|
|
253
|
+
}
|
|
@@ -8,6 +8,8 @@ import { getNotificationService } from "../services/notification/notification.se
|
|
|
8
8
|
import { getScrobblingService } from "../services/scrobbling/scrobbling.service.js";
|
|
9
9
|
import { getDiscordRpcService } from "../services/discord/discord-rpc.service.js";
|
|
10
10
|
import { getMprisService } from "../services/mpris/mpris.service.js";
|
|
11
|
+
import { getWebServerManager } from "../services/web/web-server-manager.js";
|
|
12
|
+
import { getWebStreamingService } from "../services/web/web-streaming.service.js";
|
|
11
13
|
const initialState = {
|
|
12
14
|
currentTrack: null,
|
|
13
15
|
isPlaying: false,
|
|
@@ -551,6 +553,28 @@ export function PlayerProvider({ children }) {
|
|
|
551
553
|
// Only register handlers once, update via ref
|
|
552
554
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
553
555
|
}, []);
|
|
556
|
+
// Web streaming: Broadcast state changes to connected clients
|
|
557
|
+
useEffect(() => {
|
|
558
|
+
// Initialize web streaming service and set up command handler
|
|
559
|
+
const streamingService = getWebStreamingService();
|
|
560
|
+
// Set up handler for incoming commands from web clients
|
|
561
|
+
const unsubscribe = streamingService.onMessage(message => {
|
|
562
|
+
if (message.type === 'command') {
|
|
563
|
+
dispatch(message.action);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
return () => {
|
|
567
|
+
unsubscribe();
|
|
568
|
+
};
|
|
569
|
+
}, [dispatch]);
|
|
570
|
+
// Broadcast state changes to web clients
|
|
571
|
+
useEffect(() => {
|
|
572
|
+
const webServerManager = getWebServerManager();
|
|
573
|
+
if (webServerManager.isServerRunning()) {
|
|
574
|
+
const streamingService = getWebStreamingService();
|
|
575
|
+
streamingService.onStateChange(state);
|
|
576
|
+
}
|
|
577
|
+
}, [state]);
|
|
554
578
|
const actions = useMemo(() => ({
|
|
555
579
|
play: (track) => {
|
|
556
580
|
logger.info('PlayerProvider', 'play() action dispatched', {
|
|
@@ -11,4 +11,12 @@ export interface Flags {
|
|
|
11
11
|
showSuggestions?: boolean;
|
|
12
12
|
headless?: boolean;
|
|
13
13
|
action?: 'pause' | 'resume' | 'next' | 'previous';
|
|
14
|
+
importSource?: 'spotify' | 'youtube';
|
|
15
|
+
importUrl?: string;
|
|
16
|
+
importName?: string;
|
|
17
|
+
web?: boolean;
|
|
18
|
+
webHost?: string;
|
|
19
|
+
webPort?: number;
|
|
20
|
+
webOnly?: boolean;
|
|
21
|
+
webAuth?: string;
|
|
14
22
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Playlist } from './youtube-music.types.ts';
|
|
2
2
|
import type { Theme } from './theme.types.ts';
|
|
3
|
+
import type { WebServerConfig } from './web.types.ts';
|
|
3
4
|
export type RepeatMode = 'off' | 'all' | 'one';
|
|
4
5
|
export type DownloadFormat = 'mp3' | 'm4a';
|
|
5
6
|
export interface KeybindingConfig {
|
|
@@ -34,4 +35,5 @@ export interface Config {
|
|
|
34
35
|
downloadsEnabled?: boolean;
|
|
35
36
|
downloadDirectory?: string;
|
|
36
37
|
downloadFormat?: DownloadFormat;
|
|
38
|
+
webServer?: WebServerConfig;
|
|
37
39
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Track } from './youtube-music.types.ts';
|
|
2
|
+
/** Supported import sources */
|
|
3
|
+
export type ImportSource = 'spotify' | 'youtube';
|
|
4
|
+
/** Import operation status */
|
|
5
|
+
export type ImportStatus = 'idle' | 'fetching' | 'matching' | 'creating' | 'completed' | 'failed' | 'cancelled';
|
|
6
|
+
/** Match confidence level */
|
|
7
|
+
export type MatchConfidence = 'high' | 'medium' | 'low' | 'none';
|
|
8
|
+
/** Import progress information */
|
|
9
|
+
export interface ImportProgress {
|
|
10
|
+
status: ImportStatus;
|
|
11
|
+
current: number;
|
|
12
|
+
total: number;
|
|
13
|
+
currentTrack?: string;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
/** Track match result */
|
|
17
|
+
export interface TrackMatch {
|
|
18
|
+
originalTrack: SpotifyTrack | YouTubeTrack;
|
|
19
|
+
matchedTrack: Track | null;
|
|
20
|
+
confidence: MatchConfidence;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Import result summary */
|
|
24
|
+
export interface ImportResult {
|
|
25
|
+
playlistId: string;
|
|
26
|
+
playlistName: string;
|
|
27
|
+
source: ImportSource;
|
|
28
|
+
total: number;
|
|
29
|
+
matched: number;
|
|
30
|
+
failed: number;
|
|
31
|
+
matches: TrackMatch[];
|
|
32
|
+
errors: string[];
|
|
33
|
+
duration: number;
|
|
34
|
+
}
|
|
35
|
+
/** Spotify track data */
|
|
36
|
+
export interface SpotifyTrack {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
artists: string[];
|
|
40
|
+
album?: string;
|
|
41
|
+
duration: number;
|
|
42
|
+
trackNumber?: number;
|
|
43
|
+
}
|
|
44
|
+
/** Spotify playlist data */
|
|
45
|
+
export interface SpotifyPlaylist {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
tracks: SpotifyTrack[];
|
|
50
|
+
isPublic: boolean;
|
|
51
|
+
owner?: string;
|
|
52
|
+
url?: string;
|
|
53
|
+
}
|
|
54
|
+
/** YouTube track data (for import) */
|
|
55
|
+
export interface YouTubeTrack {
|
|
56
|
+
id: string;
|
|
57
|
+
title: string;
|
|
58
|
+
name: string;
|
|
59
|
+
artists: string[];
|
|
60
|
+
album?: string;
|
|
61
|
+
duration: number;
|
|
62
|
+
thumbnail?: string;
|
|
63
|
+
}
|
|
64
|
+
/** YouTube playlist data (for import) */
|
|
65
|
+
export interface YouTubePlaylist {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
tracks: YouTubeTrack[];
|
|
70
|
+
channelTitle?: string;
|
|
71
|
+
url?: string;
|
|
72
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { PlayerAction, PlayerState } from './player.types.ts';
|
|
2
|
+
import type { ImportProgress, ImportResult } from './import.types.ts';
|
|
3
|
+
/** WebSocket server message types */
|
|
4
|
+
export type ServerMessage = StateUpdateMessage | EventMessage | ErrorMessage | AuthMessage | ImportProgressMessage | ImportResultMessage;
|
|
5
|
+
/** Player state update message */
|
|
6
|
+
export interface StateUpdateMessage {
|
|
7
|
+
type: 'state-update';
|
|
8
|
+
state: Partial<PlayerState>;
|
|
9
|
+
}
|
|
10
|
+
/** Event message for one-time events */
|
|
11
|
+
export interface EventMessage {
|
|
12
|
+
type: 'event';
|
|
13
|
+
event: 'connected' | 'disconnected' | 'client-connected' | 'client-disconnected';
|
|
14
|
+
data?: unknown;
|
|
15
|
+
}
|
|
16
|
+
/** Error message from server */
|
|
17
|
+
export interface ErrorMessage {
|
|
18
|
+
type: 'error';
|
|
19
|
+
error: string;
|
|
20
|
+
code?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Authentication message */
|
|
23
|
+
export interface AuthMessage {
|
|
24
|
+
type: 'auth';
|
|
25
|
+
success: boolean;
|
|
26
|
+
message?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Import progress message */
|
|
29
|
+
export interface ImportProgressMessage {
|
|
30
|
+
type: 'import-progress';
|
|
31
|
+
data: ImportProgress;
|
|
32
|
+
}
|
|
33
|
+
/** Import result message */
|
|
34
|
+
export interface ImportResultMessage {
|
|
35
|
+
type: 'import-result';
|
|
36
|
+
data: ImportResult;
|
|
37
|
+
}
|
|
38
|
+
/** WebSocket client message types */
|
|
39
|
+
export type ClientMessage = CommandMessage | AuthRequestMessage | ImportRequestMessage;
|
|
40
|
+
/** Command message from client */
|
|
41
|
+
export interface CommandMessage {
|
|
42
|
+
type: 'command';
|
|
43
|
+
action: PlayerAction;
|
|
44
|
+
}
|
|
45
|
+
/** Authentication request from client */
|
|
46
|
+
export interface AuthRequestMessage {
|
|
47
|
+
type: 'auth-request';
|
|
48
|
+
token: string;
|
|
49
|
+
}
|
|
50
|
+
/** Import request from client */
|
|
51
|
+
export interface ImportRequestMessage {
|
|
52
|
+
type: 'import-request';
|
|
53
|
+
source: 'spotify' | 'youtube';
|
|
54
|
+
url: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
}
|
|
57
|
+
/** WebSocket client information */
|
|
58
|
+
export interface WebSocketClient {
|
|
59
|
+
id: string;
|
|
60
|
+
authenticated: boolean;
|
|
61
|
+
connectedAt: number;
|
|
62
|
+
lastHeartbeat: number;
|
|
63
|
+
}
|
|
64
|
+
/** Web server configuration */
|
|
65
|
+
export interface WebServerConfig {
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
host: string;
|
|
68
|
+
port: number;
|
|
69
|
+
enableCors: boolean;
|
|
70
|
+
allowedOrigins: string[];
|
|
71
|
+
auth: {
|
|
72
|
+
enabled: boolean;
|
|
73
|
+
token?: string;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Web server options for CLI flags */
|
|
77
|
+
export interface WebServerOptions {
|
|
78
|
+
enabled: boolean;
|
|
79
|
+
host?: string;
|
|
80
|
+
port?: number;
|
|
81
|
+
webOnly?: boolean;
|
|
82
|
+
auth?: string;
|
|
83
|
+
}
|
|
84
|
+
/** Server statistics */
|
|
85
|
+
export interface ServerStats {
|
|
86
|
+
uptime: number;
|
|
87
|
+
clients: number;
|
|
88
|
+
totalConnections: number;
|
|
89
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@involvex/youtube-music-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "- A Commandline music player for youtube-music",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,7 +51,10 @@
|
|
|
51
51
|
"typecheck": "tsc --noEmit",
|
|
52
52
|
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
|
53
53
|
"clean": "rimraf dist",
|
|
54
|
-
"release": "powershell -File scripts/release.ps1"
|
|
54
|
+
"release": "powershell -File scripts/release.ps1",
|
|
55
|
+
"build:web": "cd web && bun run build",
|
|
56
|
+
"dev:web": "cd web && bun run dev",
|
|
57
|
+
"build:all": "bun run build && bun run build:web"
|
|
55
58
|
},
|
|
56
59
|
"prettier": "@vdemedes/prettier-config",
|
|
57
60
|
"ava": {
|
|
@@ -73,7 +76,8 @@
|
|
|
73
76
|
"play-sound": "^1.1.6",
|
|
74
77
|
"react": "^19.2.4",
|
|
75
78
|
"youtube-ext": "^1.1.25",
|
|
76
|
-
"youtubei.js": "^16.0.1"
|
|
79
|
+
"youtubei.js": "^16.0.1",
|
|
80
|
+
"ws": "^8.18.0"
|
|
77
81
|
},
|
|
78
82
|
"devDependencies": {
|
|
79
83
|
"@eslint/js": "^10.0.1",
|
|
@@ -81,6 +85,7 @@
|
|
|
81
85
|
"@types/node": "^25.2.3",
|
|
82
86
|
"@types/node-notifier": "^8.0.5",
|
|
83
87
|
"@types/react": "^19.2.14",
|
|
88
|
+
"@types/ws": "^8.5.13",
|
|
84
89
|
"@vdemedes/prettier-config": "^2.0.1",
|
|
85
90
|
"ava": "^6.4.1",
|
|
86
91
|
"chalk": "^5.6.2",
|