@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,271 @@
1
+ import { getMusicService } from "../youtube-music/api.js";
2
+ import { logger } from "../logger/logger.service.js";
3
+ class TrackMatcherService {
4
+ musicService = getMusicService();
5
+ searchCache = new Map();
6
+ matchCache = new Map();
7
+ /**
8
+ * Build a search query from track metadata
9
+ */
10
+ buildSearchQuery(track) {
11
+ // Combine artist and title for best results
12
+ const artists = track.artists.slice(0, 2).join(', '); // Use up to 2 artists
13
+ const name = track.name || track.title;
14
+ return `${artists} ${name}`.trim();
15
+ }
16
+ /**
17
+ * Get track name from either type
18
+ */
19
+ getTrackName(track) {
20
+ return track.name || track.title;
21
+ }
22
+ /**
23
+ * Calculate string similarity using Levenshtein distance
24
+ */
25
+ calculateSimilarity(str1, str2) {
26
+ const s1 = str1.toLowerCase().normalize();
27
+ const s2 = str2.toLowerCase().normalize();
28
+ // Exact match
29
+ if (s1 === s2)
30
+ return 1;
31
+ // Check if one contains the other
32
+ if (s1.includes(s2) || s2.includes(s1))
33
+ return 0.9;
34
+ // Simple Levenshtein-based similarity
35
+ const longer = s1.length > s2.length ? s1 : s2;
36
+ const shorter = s1.length > s2.length ? s2 : s1;
37
+ if (longer.length === 0)
38
+ return 1;
39
+ const editDistance = this.levenshteinDistance(longer, shorter);
40
+ return (longer.length - editDistance) / longer.length;
41
+ }
42
+ /**
43
+ * Calculate Levenshtein distance between two strings
44
+ */
45
+ levenshteinDistance(str1, str2) {
46
+ const matrix = [];
47
+ for (let i = 0; i <= str2.length; i++) {
48
+ matrix[i] = [i];
49
+ }
50
+ for (let j = 0; j <= str1.length; j++) {
51
+ if (matrix[0]) {
52
+ matrix[0][j] = j;
53
+ }
54
+ }
55
+ for (let i = 1; i <= str2.length; i++) {
56
+ const row = matrix[i];
57
+ if (!row)
58
+ continue;
59
+ const prevRow = matrix[i - 1];
60
+ if (!prevRow)
61
+ continue;
62
+ for (let j = 1; j <= str1.length; j++) {
63
+ if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
64
+ row[j] = prevRow[j - 1] ?? 0;
65
+ }
66
+ else {
67
+ row[j] = Math.min((prevRow[j - 1] ?? 0) + 1, // substitution
68
+ (row[j - 1] ?? 0) + 1, // insertion
69
+ (prevRow[j] ?? 0) + 1);
70
+ }
71
+ }
72
+ }
73
+ const lastRow = matrix[str2.length];
74
+ return lastRow?.[str1.length] ?? 0;
75
+ }
76
+ /**
77
+ * Check if artists match (any overlap in artist lists)
78
+ */
79
+ artistsMatch(trackArtists, candidateArtists) {
80
+ const normalizedTrackArtists = trackArtists
81
+ .map(a => a.toLowerCase().trim())
82
+ .filter(a => a.length > 0);
83
+ // candidateArtists are strings directly from the track
84
+ const normalizedCandidateArtists = candidateArtists
85
+ .map(a => a.toLowerCase().trim())
86
+ .filter(a => a.length > 0);
87
+ // Check for any artist name overlap
88
+ return normalizedTrackArtists.some(trackArtist => normalizedCandidateArtists.some(candidateArtist => candidateArtist.includes(trackArtist) ||
89
+ trackArtist.includes(candidateArtist)));
90
+ }
91
+ /**
92
+ * Calculate duration proximity score (0-1)
93
+ */
94
+ durationScore(originalDuration, candidateDuration) {
95
+ if (!originalDuration || !candidateDuration)
96
+ return 0.5; // Neutral if either is missing
97
+ const diff = Math.abs(originalDuration - candidateDuration);
98
+ const maxDiff = Math.max(originalDuration, candidateDuration) * 0.3; // 30% tolerance
99
+ if (diff === 0)
100
+ return 1;
101
+ if (diff <= maxDiff)
102
+ return 1 - diff / maxDiff;
103
+ return 0;
104
+ }
105
+ /**
106
+ * Score a track match based on multiple factors
107
+ */
108
+ scoreMatch(original, candidate) {
109
+ let score = 0;
110
+ const weights = {
111
+ title: 0.5,
112
+ artist: 0.3,
113
+ duration: 0.2,
114
+ };
115
+ // Title similarity
116
+ const titleSimilarity = this.calculateSimilarity(this.getTrackName(original), candidate.title);
117
+ score += titleSimilarity * weights.title;
118
+ // Artist match
119
+ const artistMatch = this.artistsMatch(original.artists, candidate.artists.map(a => a.name))
120
+ ? 1
121
+ : 0;
122
+ score += artistMatch * weights.artist;
123
+ // Duration proximity
124
+ const durationScore = this.durationScore(original.duration, candidate.duration ?? 0);
125
+ score += durationScore * weights.duration;
126
+ return score;
127
+ }
128
+ /**
129
+ * Determine confidence level from score
130
+ */
131
+ getConfidence(score) {
132
+ if (score >= 0.85)
133
+ return 'high';
134
+ if (score >= 0.7)
135
+ return 'medium';
136
+ if (score >= 0.5)
137
+ return 'low';
138
+ return 'none';
139
+ }
140
+ /**
141
+ * Search YouTube Music for a track
142
+ */
143
+ async searchTrack(track) {
144
+ const query = this.buildSearchQuery(track);
145
+ const cacheKey = `track:${query}`;
146
+ // Check cache first
147
+ const cached = this.searchCache.get(cacheKey);
148
+ if (cached) {
149
+ logger.debug('TrackMatcherService', 'Using cached search results', {
150
+ query,
151
+ });
152
+ return cached;
153
+ }
154
+ try {
155
+ logger.debug('TrackMatcherService', 'Searching for track', { query });
156
+ const response = await this.musicService.search(query, {
157
+ type: 'songs',
158
+ limit: 10,
159
+ });
160
+ const results = response.results
161
+ .filter(r => r.type === 'song')
162
+ .map(r => r.data);
163
+ // Cache results
164
+ this.searchCache.set(cacheKey, results);
165
+ return results;
166
+ }
167
+ catch (error) {
168
+ logger.error('TrackMatcherService', 'Track search failed', {
169
+ query,
170
+ error: error instanceof Error ? error.message : String(error),
171
+ });
172
+ return [];
173
+ }
174
+ }
175
+ /**
176
+ * Find the best matching track on YouTube Music
177
+ */
178
+ async findMatch(original) {
179
+ // Create a unique key for this track
180
+ const trackKey = `${original.artists[0] ?? ''}-${this.getTrackName(original)}-${original.duration}`;
181
+ const cached = this.matchCache.get(trackKey);
182
+ if (cached) {
183
+ logger.debug('TrackMatcherService', 'Using cached match', {
184
+ track: this.getTrackName(original),
185
+ });
186
+ return cached;
187
+ }
188
+ try {
189
+ const candidates = await this.searchTrack(original);
190
+ if (candidates.length === 0) {
191
+ const noMatch = {
192
+ originalTrack: original,
193
+ matchedTrack: null,
194
+ confidence: 'none',
195
+ };
196
+ this.matchCache.set(trackKey, noMatch);
197
+ return noMatch;
198
+ }
199
+ // Score all candidates
200
+ const scoredCandidates = candidates.map(candidate => ({
201
+ ...candidate,
202
+ score: this.scoreMatch(original, candidate),
203
+ }));
204
+ // Sort by score descending
205
+ scoredCandidates.sort((a, b) => b.score - a.score);
206
+ const best = scoredCandidates[0];
207
+ if (!best) {
208
+ const noMatch = {
209
+ originalTrack: original,
210
+ matchedTrack: null,
211
+ confidence: 'none',
212
+ };
213
+ this.matchCache.set(trackKey, noMatch);
214
+ return noMatch;
215
+ }
216
+ const { score, ...matchedTrack } = best;
217
+ const confidence = this.getConfidence(score);
218
+ const match = {
219
+ originalTrack: original,
220
+ matchedTrack: matchedTrack,
221
+ confidence,
222
+ };
223
+ logger.debug('TrackMatcherService', 'Track matched', {
224
+ original: this.getTrackName(original),
225
+ matched: matchedTrack.title,
226
+ confidence,
227
+ score,
228
+ });
229
+ this.matchCache.set(trackKey, match);
230
+ return match;
231
+ }
232
+ catch (error) {
233
+ logger.error('TrackMatcherService', 'Match finding failed', {
234
+ track: this.getTrackName(original),
235
+ error: error instanceof Error ? error.message : String(error),
236
+ });
237
+ const errorMatch = {
238
+ originalTrack: original,
239
+ matchedTrack: null,
240
+ confidence: 'none',
241
+ error: error instanceof Error ? error.message : String(error),
242
+ };
243
+ return errorMatch;
244
+ }
245
+ }
246
+ /**
247
+ * Clear all caches
248
+ */
249
+ clearCache() {
250
+ this.searchCache.clear();
251
+ this.matchCache.clear();
252
+ logger.debug('TrackMatcherService', 'Caches cleared');
253
+ }
254
+ /**
255
+ * Get cache statistics
256
+ */
257
+ getCacheStats() {
258
+ return {
259
+ searchCache: this.searchCache.size,
260
+ matchCache: this.matchCache.size,
261
+ };
262
+ }
263
+ }
264
+ // Singleton instance
265
+ let trackMatcherServiceInstance = null;
266
+ export function getTrackMatcherService() {
267
+ if (!trackMatcherServiceInstance) {
268
+ trackMatcherServiceInstance = new TrackMatcherService();
269
+ }
270
+ return trackMatcherServiceInstance;
271
+ }
@@ -0,0 +1,17 @@
1
+ import type { YouTubePlaylist } from '../../types/import.types.ts';
2
+ declare class YouTubeImportService {
3
+ /**
4
+ * Extract playlist ID from various YouTube URL formats
5
+ */
6
+ extractPlaylistId(input: string): string | null;
7
+ /**
8
+ * Fetch a YouTube playlist and normalize to import format
9
+ */
10
+ fetchPlaylist(urlOrId: string): Promise<YouTubePlaylist | null>;
11
+ /**
12
+ * Validate if a playlist is accessible (not private/unavailable)
13
+ */
14
+ validatePlaylist(urlOrId: string): Promise<boolean>;
15
+ }
16
+ export declare function getYouTubeImportService(): YouTubeImportService;
17
+ export {};
@@ -0,0 +1,84 @@
1
+ import { getMusicService } from "../youtube-music/api.js";
2
+ import { logger } from "../logger/logger.service.js";
3
+ class YouTubeImportService {
4
+ /**
5
+ * Extract playlist ID from various YouTube URL formats
6
+ */
7
+ extractPlaylistId(input) {
8
+ // Direct playlist ID
9
+ if (/^[-_A-Za-z0-9]{10,}$/.test(input)) {
10
+ return input;
11
+ }
12
+ // youtube.com/watch?v=...&list=...
13
+ const watchMatch = input.match(/[?&]list=([-_A-Za-z0-9]+)/);
14
+ if (watchMatch) {
15
+ return watchMatch[1] ?? null;
16
+ }
17
+ // youtube.com/playlist?list=...
18
+ const playlistMatch = input.match(/[?&]list=([-_A-Za-z0-9]+)/);
19
+ if (playlistMatch) {
20
+ return playlistMatch[1] ?? null;
21
+ }
22
+ return null;
23
+ }
24
+ /**
25
+ * Fetch a YouTube playlist and normalize to import format
26
+ */
27
+ async fetchPlaylist(urlOrId) {
28
+ const playlistId = this.extractPlaylistId(urlOrId);
29
+ if (!playlistId) {
30
+ logger.warn('YouTubeImportService', 'Invalid YouTube playlist URL or ID', {
31
+ input: urlOrId,
32
+ });
33
+ return null;
34
+ }
35
+ try {
36
+ logger.info('YouTubeImportService', 'Fetching YouTube playlist', {
37
+ playlistId,
38
+ });
39
+ const musicService = getMusicService();
40
+ const playlist = await musicService.getPlaylist(playlistId);
41
+ // Normalize to YouTubePlaylist import format
42
+ const normalized = {
43
+ id: playlist.playlistId,
44
+ name: playlist.name,
45
+ tracks: playlist.tracks.map(track => ({
46
+ id: track.videoId,
47
+ title: track.title,
48
+ name: track.title,
49
+ artists: track.artists.map(a => a.name),
50
+ album: track.album?.name,
51
+ duration: track.duration ?? 0,
52
+ })),
53
+ url: `https://www.youtube.com/playlist?list=${playlist.playlistId}`,
54
+ };
55
+ logger.info('YouTubeImportService', 'Successfully fetched playlist', {
56
+ playlistId,
57
+ trackCount: normalized.tracks.length,
58
+ });
59
+ return normalized;
60
+ }
61
+ catch (error) {
62
+ logger.error('YouTubeImportService', 'Failed to fetch playlist', {
63
+ playlistId,
64
+ error: error instanceof Error ? error.message : String(error),
65
+ });
66
+ return null;
67
+ }
68
+ }
69
+ /**
70
+ * Validate if a playlist is accessible (not private/unavailable)
71
+ */
72
+ async validatePlaylist(urlOrId) {
73
+ const playlist = await this.fetchPlaylist(urlOrId);
74
+ return playlist !== null && playlist.tracks.length > 0;
75
+ }
76
+ }
77
+ // Singleton instance
78
+ let youtubeImportServiceInstance = null;
79
+ export function getYouTubeImportService() {
80
+ if (!youtubeImportServiceInstance) {
81
+ youtubeImportServiceInstance = new YouTubeImportService();
82
+ }
83
+ return youtubeImportServiceInstance;
84
+ }
@@ -0,0 +1,31 @@
1
+ declare class StaticFileService {
2
+ private webDistDir;
3
+ private indexHtml;
4
+ private indexHtmlLoaded;
5
+ constructor();
6
+ /**
7
+ * Get MIME type for a file extension
8
+ */
9
+ private getMimeType;
10
+ /**
11
+ * Load index.html into memory
12
+ */
13
+ private loadIndexHtml;
14
+ /**
15
+ * Serve a static file
16
+ */
17
+ serve(url: string, _req: unknown, res: {
18
+ writeHead: (statusCode: number, headers?: Record<string, string>) => void;
19
+ end: (data?: string | Buffer) => void;
20
+ }): Promise<void>;
21
+ /**
22
+ * Check if web UI is built
23
+ */
24
+ isWebUiBuilt(): boolean;
25
+ /**
26
+ * Clear cached index.html (useful for development)
27
+ */
28
+ clearCache(): void;
29
+ }
30
+ export declare function getStaticFileService(): StaticFileService;
31
+ export {};
@@ -0,0 +1,174 @@
1
+ // Static file serving service for web UI
2
+ import { readFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { extname, join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { logger } from "../logger/logger.service.js";
7
+ const MIME_TYPES = {
8
+ '.html': 'text/html',
9
+ '.css': 'text/css',
10
+ '.js': 'text/javascript',
11
+ '.json': 'application/json',
12
+ '.png': 'image/png',
13
+ '.jpg': 'image/jpeg',
14
+ '.jpeg': 'image/jpeg',
15
+ '.gif': 'image/gif',
16
+ '.svg': 'image/svg+xml',
17
+ '.ico': 'image/x-icon',
18
+ '.woff': 'font/woff',
19
+ '.woff2': 'font/woff2',
20
+ '.ttf': 'font/ttf',
21
+ '.eot': 'application/vnd.ms-fontobject',
22
+ };
23
+ class StaticFileService {
24
+ webDistDir;
25
+ indexHtml = null;
26
+ indexHtmlLoaded = false;
27
+ constructor() {
28
+ // Web UI is built to dist/web/ relative to the project root
29
+ // Get the directory of the current file
30
+ const currentFile = fileURLToPath(import.meta.url);
31
+ const currentDir = dirname(currentFile);
32
+ // Detect if running from dist/ or source/
33
+ // dist/source/services/web -> need to go up 4 levels to reach project root
34
+ // source/services/web -> need to go up 3 levels to reach project root
35
+ const isDist = currentFile.includes('/dist/') || currentFile.includes('\\dist\\');
36
+ let projectRoot;
37
+ if (isDist) {
38
+ // dist/source/services/web -> services/web -> services -> source -> dist -> project root
39
+ projectRoot = join(currentDir, '..', '..', '..', '..');
40
+ }
41
+ else {
42
+ // source/services/web -> services/web -> services -> source -> project root
43
+ projectRoot = join(currentDir, '..', '..', '..');
44
+ }
45
+ this.webDistDir = join(projectRoot, 'dist', 'web');
46
+ console.log('[StaticFileService] Path resolution:', {
47
+ currentFile,
48
+ currentDir,
49
+ isDist,
50
+ projectRoot,
51
+ webDistDir: this.webDistDir,
52
+ exists: existsSync(this.webDistDir),
53
+ });
54
+ }
55
+ /**
56
+ * Get MIME type for a file extension
57
+ */
58
+ getMimeType(filePath) {
59
+ const ext = extname(filePath).toLowerCase();
60
+ return MIME_TYPES[ext] || 'application/octet-stream';
61
+ }
62
+ /**
63
+ * Load index.html into memory
64
+ */
65
+ async loadIndexHtml() {
66
+ if (this.indexHtmlLoaded)
67
+ return;
68
+ 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
+ try {
75
+ const buffer = await readFile(indexPath);
76
+ this.indexHtml = buffer.toString('utf-8');
77
+ this.indexHtmlLoaded = true;
78
+ logger.debug('StaticFileService', 'index.html loaded', { indexPath });
79
+ console.log('[StaticFileService] index.html loaded successfully');
80
+ }
81
+ catch (error) {
82
+ console.error('[StaticFileService] Failed to load index.html:', error);
83
+ logger.error('StaticFileService', 'Failed to load index.html', {
84
+ indexPath,
85
+ error: error instanceof Error ? error.message : String(error),
86
+ });
87
+ }
88
+ }
89
+ /**
90
+ * Serve a static file
91
+ */
92
+ async serve(url, _req, res) {
93
+ // Remove query string
94
+ const urlPath = url.split('?')[0] ?? '/';
95
+ // Serve index.html for SPA routes
96
+ if (urlPath === '/' || !urlPath.includes('.')) {
97
+ // Ensure index.html is loaded
98
+ if (!this.indexHtmlLoaded) {
99
+ await this.loadIndexHtml();
100
+ }
101
+ if (this.indexHtml) {
102
+ res.writeHead(200, {
103
+ 'Content-Type': 'text/html',
104
+ 'Cache-Control': 'public, max-age=3600',
105
+ });
106
+ res.end(this.indexHtml);
107
+ }
108
+ else {
109
+ // Web UI not built, serve a simple message
110
+ res.writeHead(503, { 'Content-Type': 'text/html' });
111
+ res.end(`
112
+ <!DOCTYPE html>
113
+ <html>
114
+ <head><title>Web UI Not Built</title></head>
115
+ <body>
116
+ <h1>Web UI Not Built</h1>
117
+ <p>Run <code>bun run build:web</code> to build the web UI.</p>
118
+ </body>
119
+ </html>
120
+ `);
121
+ }
122
+ return;
123
+ }
124
+ // Serve static files
125
+ const filePath = join(this.webDistDir, urlPath);
126
+ try {
127
+ // Check if file exists
128
+ if (!existsSync(filePath)) {
129
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
130
+ res.end('Not Found');
131
+ return;
132
+ }
133
+ // Read and serve file
134
+ const content = await readFile(filePath);
135
+ const mimeType = this.getMimeType(filePath);
136
+ res.writeHead(200, {
137
+ 'Content-Type': mimeType,
138
+ 'Cache-Control': 'public, max-age=86400', // 1 day
139
+ });
140
+ res.end(content);
141
+ }
142
+ catch (error) {
143
+ logger.error('StaticFileService', 'Failed to serve file', {
144
+ filePath,
145
+ error: error instanceof Error ? error.message : String(error),
146
+ });
147
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
148
+ res.end('Internal Server Error');
149
+ }
150
+ }
151
+ /**
152
+ * Check if web UI is built
153
+ */
154
+ isWebUiBuilt() {
155
+ const indexPath = join(this.webDistDir, 'index.html');
156
+ return existsSync(indexPath);
157
+ }
158
+ /**
159
+ * Clear cached index.html (useful for development)
160
+ */
161
+ clearCache() {
162
+ this.indexHtml = null;
163
+ this.indexHtmlLoaded = false;
164
+ logger.debug('StaticFileService', 'Cache cleared');
165
+ }
166
+ }
167
+ // Singleton instance
168
+ let staticFileServiceInstance = null;
169
+ export function getStaticFileService() {
170
+ if (!staticFileServiceInstance) {
171
+ staticFileServiceInstance = new StaticFileService();
172
+ }
173
+ return staticFileServiceInstance;
174
+ }
@@ -0,0 +1,66 @@
1
+ import type { WebServerConfig, WebServerOptions } from '../../types/web.types.ts';
2
+ import type { PlayerState } from '../../types/player.types.ts';
3
+ declare class WebServerManager {
4
+ private config;
5
+ private isRunning;
6
+ private cleanupHooks;
7
+ constructor();
8
+ /**
9
+ * Start the web server
10
+ */
11
+ start(options?: WebServerOptions): Promise<void>;
12
+ /**
13
+ * Stop the web server
14
+ */
15
+ stop(): Promise<void>;
16
+ /**
17
+ * Set up player command handler
18
+ */
19
+ private setupCommandHandler;
20
+ /**
21
+ * Set up import progress handler
22
+ */
23
+ private setupImportHandler;
24
+ /**
25
+ * Handle command from web client
26
+ */
27
+ private handleCommand;
28
+ /**
29
+ * Handle import request from web client
30
+ */
31
+ private handleImportRequest;
32
+ /**
33
+ * Update player state (call this when player state changes)
34
+ */
35
+ updateState(state: PlayerState): void;
36
+ /**
37
+ * Set up graceful shutdown hooks
38
+ */
39
+ private setupShutdownHooks;
40
+ /**
41
+ * Check if server is running
42
+ */
43
+ isServerRunning(): boolean;
44
+ /**
45
+ * Get server URL
46
+ */
47
+ getServerUrl(): string;
48
+ /**
49
+ * Get server statistics
50
+ */
51
+ getStats(): {
52
+ running: boolean;
53
+ url?: string;
54
+ clients?: number;
55
+ };
56
+ /**
57
+ * Update configuration
58
+ */
59
+ updateConfig(config: Partial<WebServerConfig>): void;
60
+ /**
61
+ * Get current configuration
62
+ */
63
+ getConfig(): WebServerConfig;
64
+ }
65
+ export declare function getWebServerManager(): WebServerManager;
66
+ export {};