@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.
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/dist/source/cli.js +21 -0
- package/dist/source/components/export/ExportLayout.d.ts +1 -0
- package/dist/source/components/export/ExportLayout.js +111 -0
- package/dist/source/components/layouts/MainLayout.js +29 -4
- package/dist/source/components/settings/Settings.js +6 -2
- package/dist/source/main.js +20 -0
- package/dist/source/services/config/config.service.d.ts +15 -0
- package/dist/source/services/config/config.service.js +31 -0
- package/dist/source/services/export/export.service.d.ts +41 -0
- package/dist/source/services/export/export.service.js +131 -0
- package/dist/source/services/player/player.service.d.ts +12 -0
- package/dist/source/services/player/player.service.js +30 -0
- package/dist/source/services/version-check/version-check.service.d.ts +32 -0
- package/dist/source/services/version-check/version-check.service.js +121 -0
- package/dist/source/services/web/static-file.service.js +2 -13
- package/dist/source/services/web/web-server-manager.d.ts +22 -0
- package/dist/source/services/web/web-server-manager.js +285 -2
- package/dist/source/services/web/websocket.server.d.ts +6 -1
- package/dist/source/services/web/websocket.server.js +14 -0
- package/dist/source/types/actions.d.ts +3 -0
- package/dist/source/types/config.types.d.ts +7 -0
- package/dist/source/types/navigation.types.d.ts +2 -2
- package/dist/source/types/web.types.d.ts +40 -2
- package/dist/source/utils/constants.d.ts +3 -1
- package/dist/source/utils/constants.js +3 -1
- package/dist/youtube-music-cli.exe +0 -0
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
121
|
-
|
|
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:
|
|
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;
|
|
@@ -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;
|