@involvex/youtube-music-cli 0.0.19 → 0.0.21

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 (29) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +21 -0
  3. package/dist/source/cli.js +21 -0
  4. package/dist/source/components/export/ExportLayout.d.ts +1 -0
  5. package/dist/source/components/export/ExportLayout.js +111 -0
  6. package/dist/source/components/layouts/MainLayout.js +29 -4
  7. package/dist/source/components/settings/Settings.js +6 -2
  8. package/dist/source/main.js +20 -0
  9. package/dist/source/services/config/config.service.d.ts +15 -0
  10. package/dist/source/services/config/config.service.js +31 -0
  11. package/dist/source/services/export/export.service.d.ts +41 -0
  12. package/dist/source/services/export/export.service.js +131 -0
  13. package/dist/source/services/player/player.service.d.ts +12 -0
  14. package/dist/source/services/player/player.service.js +30 -0
  15. package/dist/source/services/version-check/version-check.service.d.ts +32 -0
  16. package/dist/source/services/version-check/version-check.service.js +121 -0
  17. package/dist/source/services/web/static-file.service.js +2 -13
  18. package/dist/source/services/web/web-server-manager.d.ts +22 -0
  19. package/dist/source/services/web/web-server-manager.js +285 -2
  20. package/dist/source/services/web/websocket.server.d.ts +6 -1
  21. package/dist/source/services/web/websocket.server.js +14 -0
  22. package/dist/source/types/actions.d.ts +3 -0
  23. package/dist/source/types/config.types.d.ts +7 -0
  24. package/dist/source/types/navigation.types.d.ts +2 -2
  25. package/dist/source/types/web.types.d.ts +40 -2
  26. package/dist/source/utils/constants.d.ts +3 -1
  27. package/dist/source/utils/constants.js +3 -1
  28. package/dist/youtube-music-cli.exe +0 -0
  29. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ // Version check service for npm registry updates
2
+ import { APP_NAME, APP_VERSION } from "../../utils/constants.js";
3
+ import { logger } from "../logger/logger.service.js";
4
+ class VersionCheckService {
5
+ static instance;
6
+ NPM_REGISTRY_URL = 'https://registry.npmjs.org';
7
+ CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
8
+ constructor() { }
9
+ static getInstance() {
10
+ if (!VersionCheckService.instance) {
11
+ VersionCheckService.instance = new VersionCheckService();
12
+ }
13
+ return VersionCheckService.instance;
14
+ }
15
+ /**
16
+ * Compare two semantic version strings
17
+ * Returns: 1 if a > b, -1 if a < b, 0 if equal
18
+ */
19
+ compareVersions(a, b) {
20
+ const parseVersion = (v) => {
21
+ // Remove 'v' prefix if present and split by non-numeric chars
22
+ const clean = v.replace(/^v/i, '');
23
+ return clean.split(/[.-]/).map(part => {
24
+ const num = parseInt(part, 10);
25
+ return Number.isNaN(num) ? 0 : num;
26
+ });
27
+ };
28
+ const partsA = parseVersion(a);
29
+ const partsB = parseVersion(b);
30
+ const maxLength = Math.max(partsA.length, partsB.length);
31
+ for (let i = 0; i < maxLength; i++) {
32
+ const partA = partsA[i] ?? 0;
33
+ const partB = partsB[i] ?? 0;
34
+ if (partA > partB)
35
+ return 1;
36
+ if (partA < partB)
37
+ return -1;
38
+ }
39
+ return 0;
40
+ }
41
+ /**
42
+ * Check if a version check should be performed (once per 24 hours)
43
+ */
44
+ shouldCheck(lastCheck) {
45
+ if (!lastCheck)
46
+ return true;
47
+ try {
48
+ const lastCheckDate = new Date(lastCheck);
49
+ const now = new Date();
50
+ const diff = now.getTime() - lastCheckDate.getTime();
51
+ return diff >= this.CHECK_INTERVAL;
52
+ }
53
+ catch {
54
+ return true;
55
+ }
56
+ }
57
+ /**
58
+ * Mark that a version check has been performed
59
+ * Returns the timestamp string to store
60
+ */
61
+ markChecked() {
62
+ return new Date().toISOString();
63
+ }
64
+ /**
65
+ * Check npm registry for available updates
66
+ */
67
+ async checkForUpdates(currentVersion = APP_VERSION) {
68
+ try {
69
+ logger.debug('VersionCheckService', 'Checking for updates', {
70
+ package: APP_NAME,
71
+ currentVersion,
72
+ });
73
+ const url = `${this.NPM_REGISTRY_URL}/${APP_NAME}`;
74
+ const response = await fetch(url, {
75
+ signal: AbortSignal.timeout(5000), // 5 second timeout
76
+ });
77
+ if (!response.ok) {
78
+ logger.warn('VersionCheckService', 'Failed to fetch package info', {
79
+ status: response.status,
80
+ });
81
+ return {
82
+ hasUpdate: false,
83
+ currentVersion,
84
+ latestVersion: currentVersion,
85
+ };
86
+ }
87
+ const data = (await response.json());
88
+ const latestVersion = data['dist-tags']?.latest;
89
+ if (!latestVersion) {
90
+ logger.warn('VersionCheckService', 'No latest version found in response');
91
+ return {
92
+ hasUpdate: false,
93
+ currentVersion,
94
+ latestVersion: currentVersion,
95
+ };
96
+ }
97
+ const hasUpdate = this.compareVersions(latestVersion, currentVersion) > 0;
98
+ logger.info('VersionCheckService', 'Version check complete', {
99
+ currentVersion,
100
+ latestVersion,
101
+ hasUpdate,
102
+ });
103
+ return {
104
+ hasUpdate,
105
+ currentVersion,
106
+ latestVersion,
107
+ };
108
+ }
109
+ catch (error) {
110
+ logger.error('VersionCheckService', 'Error checking for updates', {
111
+ error: error instanceof Error ? error.message : String(error),
112
+ });
113
+ return {
114
+ hasUpdate: false,
115
+ currentVersion,
116
+ latestVersion: currentVersion,
117
+ };
118
+ }
119
+ }
120
+ }
121
+ export const getVersionCheckService = () => VersionCheckService.getInstance();
@@ -43,11 +43,7 @@ class StaticFileService {
43
43
  projectRoot = join(currentDir, '..', '..', '..');
44
44
  }
45
45
  this.webDistDir = join(projectRoot, 'dist', 'web');
46
- console.log('[StaticFileService] Path resolution:', {
47
- currentFile,
48
- currentDir,
49
- isDist,
50
- projectRoot,
46
+ logger.debug('StaticFileService', 'Path resolved', {
51
47
  webDistDir: this.webDistDir,
52
48
  exists: existsSync(this.webDistDir),
53
49
  });
@@ -66,20 +62,13 @@ class StaticFileService {
66
62
  if (this.indexHtmlLoaded)
67
63
  return;
68
64
  const indexPath = join(this.webDistDir, 'index.html');
69
- console.log('[StaticFileService] Loading index.html:', {
70
- webDistDir: this.webDistDir,
71
- indexPath,
72
- exists: existsSync(indexPath),
73
- });
74
65
  try {
75
66
  const buffer = await readFile(indexPath);
76
67
  this.indexHtml = buffer.toString('utf-8');
77
68
  this.indexHtmlLoaded = true;
78
- logger.debug('StaticFileService', 'index.html loaded', { indexPath });
79
- console.log('[StaticFileService] index.html loaded successfully');
69
+ logger.info('StaticFileService', 'index.html loaded');
80
70
  }
81
71
  catch (error) {
82
- console.error('[StaticFileService] Failed to load index.html:', error);
83
72
  logger.error('StaticFileService', 'Failed to load index.html', {
84
73
  indexPath,
85
74
  error: error instanceof Error ? error.message : String(error),
@@ -4,6 +4,7 @@ declare class WebServerManager {
4
4
  private config;
5
5
  private isRunning;
6
6
  private cleanupHooks;
7
+ private internalState;
7
8
  constructor();
8
9
  /**
9
10
  * Start the web server
@@ -29,10 +30,31 @@ declare class WebServerManager {
29
30
  * Handle import request from web client
30
31
  */
31
32
  private handleImportRequest;
33
+ /**
34
+ * Handle search request from web client
35
+ */
36
+ private handleSearchRequest;
37
+ /**
38
+ * Handle config update from web client
39
+ */
40
+ private handleConfigUpdate;
32
41
  /**
33
42
  * Update player state (call this when player state changes)
43
+ * This is called by PlayerProvider in normal mode to sync state
34
44
  */
35
45
  updateState(state: PlayerState): void;
46
+ /**
47
+ * Broadcast current state to all connected clients
48
+ */
49
+ private broadcastState;
50
+ /**
51
+ * Get current internal state
52
+ */
53
+ getState(): PlayerState;
54
+ /**
55
+ * Set internal state directly (for sync from external sources)
56
+ */
57
+ setState(state: Partial<PlayerState>): void;
36
58
  /**
37
59
  * Set up graceful shutdown hooks
38
60
  */
@@ -2,11 +2,28 @@ import { getWebSocketServer } from "./websocket.server.js";
2
2
  import { getWebStreamingService } from "./web-streaming.service.js";
3
3
  import { getConfigService } from "../config/config.service.js";
4
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";
5
7
  import { logger } from "../logger/logger.service.js";
6
8
  class WebServerManager {
7
9
  config;
8
10
  isRunning = false;
9
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
+ };
10
27
  constructor() {
11
28
  // Load config or use defaults
12
29
  const configService = getConfigService();
@@ -23,6 +40,8 @@ class WebServerManager {
23
40
  if (!savedConfig) {
24
41
  configService.set('webServer', this.config);
25
42
  }
43
+ // Initialize volume from config
44
+ this.internalState.volume = configService.get('volume') ?? 70;
26
45
  }
27
46
  /**
28
47
  * Start the web server
@@ -60,6 +79,8 @@ class WebServerManager {
60
79
  config: finalConfig,
61
80
  onCommand: this.handleCommand.bind(this),
62
81
  onImportRequest: this.handleImportRequest.bind(this),
82
+ onSearchRequest: this.handleSearchRequest.bind(this),
83
+ onConfigUpdate: this.handleConfigUpdate.bind(this),
63
84
  });
64
85
  this.isRunning = true;
65
86
  // Set up graceful shutdown
@@ -117,8 +138,195 @@ class WebServerManager {
117
138
  * Handle command from web client
118
139
  */
119
140
  handleCommand(action) {
120
- // This will be handled by the player store
121
- logger.debug('WebServerManager', 'Received command from client', { 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();
122
330
  }
123
331
  /**
124
332
  * Handle import request from web client
@@ -141,15 +349,90 @@ class WebServerManager {
141
349
  });
142
350
  }
143
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
+ }
144
402
  /**
145
403
  * Update player state (call this when player state changes)
404
+ * This is called by PlayerProvider in normal mode to sync state
146
405
  */
147
406
  updateState(state) {
148
407
  if (!this.isRunning)
149
408
  return;
409
+ // Update internal state to stay in sync
410
+ this.internalState = { ...state };
150
411
  const streamingService = getWebStreamingService();
151
412
  streamingService.onStateChange(state);
152
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
+ }
153
436
  /**
154
437
  * Set up graceful shutdown hooks
155
438
  */
@@ -1,8 +1,11 @@
1
1
  import type { WebServerConfig } from '../../types/web.types.ts';
2
+ import type { PlayerAction } from '../../types/player.types.ts';
2
3
  interface WebSocketServerOptions {
3
4
  config: WebServerConfig;
4
- onCommand?: (action: unknown) => void;
5
+ onCommand?: (action: PlayerAction) => void;
5
6
  onImportRequest?: (source: 'spotify' | 'youtube', url: string, name?: string) => void;
7
+ onSearchRequest?: (query: string, searchType: 'all' | 'songs' | 'artists' | 'albums' | 'playlists') => void;
8
+ onConfigUpdate?: (config: Record<string, unknown>) => void;
6
9
  }
7
10
  declare class WebSocketServerClass {
8
11
  private httpServer;
@@ -12,6 +15,8 @@ declare class WebSocketServerClass {
12
15
  private staticFileService;
13
16
  private onCommand?;
14
17
  private onImportRequest?;
18
+ private onSearchRequest?;
19
+ private onConfigUpdate?;
15
20
  constructor();
16
21
  /**
17
22
  * Start the WebSocket server
@@ -12,6 +12,8 @@ class WebSocketServerClass {
12
12
  staticFileService = getStaticFileService();
13
13
  onCommand;
14
14
  onImportRequest;
15
+ onSearchRequest;
16
+ onConfigUpdate;
15
17
  constructor() {
16
18
  this.config = {
17
19
  enabled: false,
@@ -29,6 +31,8 @@ class WebSocketServerClass {
29
31
  this.config = options.config;
30
32
  this.onCommand = options.onCommand;
31
33
  this.onImportRequest = options.onImportRequest;
34
+ this.onSearchRequest = options.onSearchRequest;
35
+ this.onConfigUpdate = options.onConfigUpdate;
32
36
  logger.info('WebSocketServer', 'Starting server', {
33
37
  host: this.config.host,
34
38
  port: this.config.port,
@@ -164,6 +168,16 @@ class WebSocketServerClass {
164
168
  this.onImportRequest(message.source, message.url, message.name);
165
169
  }
166
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;
167
181
  case 'auth-request':
168
182
  // Already handled in connection phase
169
183
  break;
@@ -130,3 +130,6 @@ export interface SetSearchLimitAction {
130
130
  export interface TogglePlayerModeAction {
131
131
  readonly category: 'TOGGLE_PLAYER_MODE';
132
132
  }
133
+ export interface DetachAction {
134
+ readonly category: 'DETACH';
135
+ }
@@ -36,4 +36,11 @@ export interface Config {
36
36
  downloadDirectory?: string;
37
37
  downloadFormat?: DownloadFormat;
38
38
  webServer?: WebServerConfig;
39
+ backgroundPlayback?: {
40
+ enabled: boolean;
41
+ ipcPath?: string;
42
+ currentUrl?: string;
43
+ timestamp?: string;
44
+ };
45
+ lastVersionCheck?: string;
39
46
  }
@@ -1,4 +1,4 @@
1
- import type { NavigateAction, GoBackAction, SetSearchQueryAction, SetSearchCategoryAction, SetSelectedResultAction, SetSelectedPlaylistAction, SetHasSearchedAction, SetSearchLimitAction, TogglePlayerModeAction } from './actions.ts';
1
+ import type { NavigateAction, GoBackAction, SetSearchQueryAction, SetSearchCategoryAction, SetSelectedResultAction, SetSelectedPlaylistAction, SetHasSearchedAction, SetSearchLimitAction, TogglePlayerModeAction, DetachAction } from './actions.ts';
2
2
  export interface NavigationState {
3
3
  currentView: string;
4
4
  previousView: string | null;
@@ -12,4 +12,4 @@ export interface NavigationState {
12
12
  history: string[];
13
13
  playerMode: 'full' | 'mini';
14
14
  }
15
- export type NavigationAction = NavigateAction | GoBackAction | SetSearchQueryAction | SetSearchCategoryAction | SetSelectedResultAction | SetSelectedPlaylistAction | SetHasSearchedAction | SetSearchLimitAction | TogglePlayerModeAction;
15
+ export type NavigationAction = NavigateAction | GoBackAction | SetSearchQueryAction | SetSearchCategoryAction | SetSelectedResultAction | SetSelectedPlaylistAction | SetHasSearchedAction | SetSearchLimitAction | TogglePlayerModeAction | DetachAction;
@@ -1,7 +1,8 @@
1
1
  import type { PlayerAction, PlayerState } from './player.types.ts';
2
2
  import type { ImportProgress, ImportResult } from './import.types.ts';
3
+ import type { Track, Album, Artist, Playlist } from './youtube-music.types.ts';
3
4
  /** WebSocket server message types */
4
- export type ServerMessage = StateUpdateMessage | EventMessage | ErrorMessage | AuthMessage | ImportProgressMessage | ImportResultMessage;
5
+ export type ServerMessage = StateUpdateMessage | EventMessage | ErrorMessage | AuthMessage | ImportProgressMessage | ImportResultMessage | SearchResultsMessage | ConfigUpdateMessage;
5
6
  /** Player state update message */
6
7
  export interface StateUpdateMessage {
7
8
  type: 'state-update';
@@ -35,8 +36,23 @@ export interface ImportResultMessage {
35
36
  type: 'import-result';
36
37
  data: ImportResult;
37
38
  }
39
+ /** Search results message */
40
+ export interface SearchResultsMessage {
41
+ type: 'search-results';
42
+ results: SearchResult[];
43
+ }
44
+ /** Search result item */
45
+ export interface SearchResult {
46
+ type: 'song' | 'album' | 'artist' | 'playlist';
47
+ data: Track | Album | Artist | Playlist;
48
+ }
49
+ /** Configuration update message */
50
+ export interface ConfigUpdateMessage {
51
+ type: 'config-update';
52
+ config: Partial<Config>;
53
+ }
38
54
  /** WebSocket client message types */
39
- export type ClientMessage = CommandMessage | AuthRequestMessage | ImportRequestMessage;
55
+ export type ClientMessage = CommandMessage | AuthRequestMessage | ImportRequestMessage | SearchRequestMessage | ConfigUpdateRequestMessage;
40
56
  /** Command message from client */
41
57
  export interface CommandMessage {
42
58
  type: 'command';
@@ -54,6 +70,28 @@ export interface ImportRequestMessage {
54
70
  url: string;
55
71
  name?: string;
56
72
  }
73
+ /** Search request from client */
74
+ export interface SearchRequestMessage {
75
+ type: 'search-request';
76
+ query: string;
77
+ searchType: 'all' | 'songs' | 'artists' | 'albums' | 'playlists';
78
+ }
79
+ /** Config update request from client */
80
+ export interface ConfigUpdateRequestMessage {
81
+ type: 'config-update';
82
+ config: Partial<Config>;
83
+ }
84
+ /** Configuration interface */
85
+ export interface Config {
86
+ theme: string;
87
+ volume: number;
88
+ repeat: 'off' | 'all' | 'one';
89
+ shuffle: boolean;
90
+ streamQuality: 'low' | 'medium' | 'high';
91
+ audioNormalization: boolean;
92
+ notifications: boolean;
93
+ discordRichPresence: boolean;
94
+ }
57
95
  /** WebSocket client information */
58
96
  export interface WebSocketClient {
59
97
  id: string;