loukai-app 0.3.22 → 0.4.2
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/README.md +5 -5
- package/package.json +2 -2
- package/src/main/audioEngine.js +11 -10
- package/src/main/creator/conversionService.js +9 -8
- package/src/main/creator/downloadManager.js +70 -67
- package/src/main/creator/ffmpegService.js +3 -2
- package/src/main/creator/installLogger.js +5 -7
- package/src/main/creator/llmService.js +13 -7
- package/src/main/creator/lrclibService.js +5 -6
- package/src/main/creator/pythonRunner.js +3 -2
- package/src/main/creator/stemBuilder.js +29 -28
- package/src/main/handlers/audioHandlers.js +2 -1
- package/src/main/handlers/canvasHandlers.js +4 -3
- package/src/main/handlers/creatorHandlers.js +2 -1
- package/src/main/handlers/editorHandlers.js +8 -7
- package/src/main/handlers/fileHandlers.js +1 -1
- package/src/main/handlers/index.js +21 -19
- package/src/main/handlers/libraryHandlers.js +3 -0
- package/src/main/handlers/queueHandlers.js +3 -2
- package/src/main/handlers/settingsHandlers.js +2 -1
- package/src/main/handlers/webServerHandlers.js +3 -2
- package/src/main/logger.js +12 -0
- package/src/main/main.js +71 -66
- package/src/main/statePersistence.js +13 -12
- package/src/main/utils/pathValidator.js +21 -17
- package/src/main/webServer.js +102 -95
- package/src/renderer/components/creator/CreateTab.jsx +6 -6
- package/src/renderer/dist/assets/{kaiPlayer-CoMx__a_.js → kaiPlayer-DSaY7TxC.js} +2 -2
- package/src/renderer/dist/assets/kaiPlayer-DSaY7TxC.js.map +1 -0
- package/src/renderer/dist/assets/songLoaders-CcYVonLu.js +2 -0
- package/src/renderer/dist/assets/{songLoaders-BaTgGib4.js.map → songLoaders-CcYVonLu.js.map} +1 -1
- package/src/renderer/dist/renderer.css +1 -1
- package/src/renderer/dist/renderer.js +11 -51
- package/src/renderer/dist/renderer.js.map +1 -1
- package/src/renderer/js/kaiPlayer.js +9 -7
- package/src/renderer/lib/cdgraphics.js +0 -1
- package/src/shared/services/creatorService.js +2 -2
- package/src/shared/services/libraryService.js +4 -1
- package/src/shared/services/serverSettingsService.js +0 -1
- package/src/shared/services/settingsService.js +0 -2
- package/src/utils/m4aLoader.js +1 -1
- package/src/web/dist/assets/index-CGbmW1VG.js +11 -0
- package/src/web/dist/assets/{index-0H-RnRrV.js.map → index-CGbmW1VG.js.map} +1 -1
- package/src/web/dist/assets/index-GLKJK41r.css +1 -0
- package/src/web/dist/index.html +2 -2
- package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +0 -1
- package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +0 -2
- package/src/web/dist/assets/index-0H-RnRrV.js +0 -51
- package/src/web/dist/assets/index-DYW2zB0u.css +0 -1
package/src/main/webServer.js
CHANGED
|
@@ -11,6 +11,7 @@ import { Server } from 'socket.io';
|
|
|
11
11
|
import http from 'http';
|
|
12
12
|
import rateLimit from 'express-rate-limit';
|
|
13
13
|
import Fuse from 'fuse.js';
|
|
14
|
+
import { log } from './logger.js';
|
|
14
15
|
import * as queueService from '../shared/services/queueService.js';
|
|
15
16
|
import * as libraryService from '../shared/services/libraryService.js';
|
|
16
17
|
import * as playerService from '../shared/services/playerService.js';
|
|
@@ -63,20 +64,20 @@ class WebServer {
|
|
|
63
64
|
*/
|
|
64
65
|
generateSongId(songPath) {
|
|
65
66
|
if (!songPath) return null;
|
|
66
|
-
|
|
67
|
+
|
|
67
68
|
// Check cache first
|
|
68
69
|
if (this.songPathToId.has(songPath)) {
|
|
69
70
|
return this.songPathToId.get(songPath);
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
|
|
72
73
|
// Create a short, URL-safe hash
|
|
73
74
|
const hash = crypto.createHash('sha256').update(songPath).digest('base64url').slice(0, 16);
|
|
74
75
|
const id = `song_${hash}`;
|
|
75
|
-
|
|
76
|
+
|
|
76
77
|
// Cache both directions
|
|
77
78
|
this.songPathToId.set(songPath, id);
|
|
78
79
|
this.songIdToPath.set(id, songPath);
|
|
79
|
-
|
|
80
|
+
|
|
80
81
|
return id;
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -93,9 +94,9 @@ class WebServer {
|
|
|
93
94
|
*/
|
|
94
95
|
sanitizeSongForPublic(song) {
|
|
95
96
|
if (!song) return null;
|
|
96
|
-
|
|
97
|
+
|
|
97
98
|
const id = this.generateSongId(song.path);
|
|
98
|
-
|
|
99
|
+
|
|
99
100
|
return {
|
|
100
101
|
id,
|
|
101
102
|
title: song.title || 'Unknown Title',
|
|
@@ -114,8 +115,8 @@ class WebServer {
|
|
|
114
115
|
*/
|
|
115
116
|
sanitizeQueueForPublic(queue) {
|
|
116
117
|
if (!Array.isArray(queue)) return [];
|
|
117
|
-
|
|
118
|
-
return queue.map(item => ({
|
|
118
|
+
|
|
119
|
+
return queue.map((item) => ({
|
|
119
120
|
position: item.position,
|
|
120
121
|
singerName: item.singerName,
|
|
121
122
|
song: item.song ? this.sanitizeSongForPublic(item.song) : null,
|
|
@@ -128,23 +129,25 @@ class WebServer {
|
|
|
128
129
|
setupMiddleware() {
|
|
129
130
|
// CORS configuration - restrict to localhost and LAN origins
|
|
130
131
|
// Prevents malicious websites from making cross-origin requests
|
|
131
|
-
this.app.use(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
132
|
+
this.app.use(
|
|
133
|
+
cors({
|
|
134
|
+
origin: (origin, callback) => {
|
|
135
|
+
// Allow requests with no origin (same-origin, non-browser clients, curl, etc.)
|
|
136
|
+
if (!origin) {
|
|
137
|
+
return callback(null, true);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this.isAllowedOrigin(origin)) {
|
|
141
|
+
return callback(null, true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Reject other origins
|
|
145
|
+
callback(new Error('CORS not allowed for this origin'));
|
|
146
|
+
},
|
|
147
|
+
credentials: true,
|
|
148
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
149
|
+
})
|
|
150
|
+
);
|
|
148
151
|
this.app.use(express.json());
|
|
149
152
|
this.app.use(express.urlencoded({ extended: true }));
|
|
150
153
|
|
|
@@ -313,11 +316,11 @@ class WebServer {
|
|
|
313
316
|
// Get available letters for alphabet navigation
|
|
314
317
|
this.app.get('/api/letters', async (req, res) => {
|
|
315
318
|
try {
|
|
316
|
-
|
|
319
|
+
log('API: Getting available letters...');
|
|
317
320
|
|
|
318
321
|
// Get songs from cache
|
|
319
322
|
const allSongs = await this.getCachedSongs();
|
|
320
|
-
|
|
323
|
+
log(`API: Found ${allSongs.length} songs`);
|
|
321
324
|
|
|
322
325
|
// Group by first letter of artist
|
|
323
326
|
const letterCounts = {};
|
|
@@ -356,7 +359,7 @@ class WebServer {
|
|
|
356
359
|
const page = parseInt(req.query.page) || 1;
|
|
357
360
|
const limit = parseInt(req.query.limit) || 100;
|
|
358
361
|
|
|
359
|
-
|
|
362
|
+
log(`API: Getting songs for letter ${letter}, page ${page}, limit ${limit}`);
|
|
360
363
|
|
|
361
364
|
// Get songs from cache
|
|
362
365
|
const allSongs = await this.getCachedSongs();
|
|
@@ -408,12 +411,12 @@ class WebServer {
|
|
|
408
411
|
const search = req.query.search || '';
|
|
409
412
|
const limit = parseInt(req.query.limit) || 50;
|
|
410
413
|
|
|
411
|
-
|
|
414
|
+
log('API: Getting songs from cache...');
|
|
412
415
|
|
|
413
416
|
// Get songs from cache
|
|
414
417
|
const allSongs = await this.getCachedSongs();
|
|
415
418
|
|
|
416
|
-
|
|
419
|
+
log(`API: Found ${allSongs.length} songs`);
|
|
417
420
|
|
|
418
421
|
let songs = allSongs;
|
|
419
422
|
|
|
@@ -476,7 +479,9 @@ class WebServer {
|
|
|
476
479
|
|
|
477
480
|
// Use fuzzy search
|
|
478
481
|
const fuseResults = this.fuse.search(query);
|
|
479
|
-
const results = fuseResults
|
|
482
|
+
const results = fuseResults
|
|
483
|
+
.slice(0, limit)
|
|
484
|
+
.map((result) => this.sanitizeSongForPublic(result.item));
|
|
480
485
|
|
|
481
486
|
res.json({ results });
|
|
482
487
|
} catch (error) {
|
|
@@ -488,17 +493,17 @@ class WebServer {
|
|
|
488
493
|
// Submit song request (with rate limiting)
|
|
489
494
|
this.app.post('/api/request', this.apiLimiter, async (req, res) => {
|
|
490
495
|
try {
|
|
491
|
-
|
|
496
|
+
log('🎤 NEW REQUEST received:', req.body);
|
|
492
497
|
|
|
493
498
|
if (!this.settings.allowSongRequests) {
|
|
494
|
-
|
|
499
|
+
log('❌ REQUEST DENIED: requests disabled');
|
|
495
500
|
return res.status(403).json({ error: 'Song requests are currently disabled' });
|
|
496
501
|
}
|
|
497
502
|
|
|
498
503
|
const { songId, requesterName, message } = req.body;
|
|
499
504
|
|
|
500
505
|
if (!songId || !requesterName) {
|
|
501
|
-
|
|
506
|
+
log('❌ REQUEST DENIED: missing required fields', {
|
|
502
507
|
songId: Boolean(songId),
|
|
503
508
|
requesterName: Boolean(requesterName),
|
|
504
509
|
});
|
|
@@ -507,26 +512,26 @@ class WebServer {
|
|
|
507
512
|
|
|
508
513
|
// Find the song in the library
|
|
509
514
|
// Support both opaque IDs (new) and paths (legacy/admin)
|
|
510
|
-
|
|
515
|
+
log('🔍 Looking for song with ID:', songId);
|
|
511
516
|
const allSongs = await this.getCachedSongs();
|
|
512
|
-
|
|
513
|
-
|
|
517
|
+
log('📚 Found library with', allSongs.length, 'songs');
|
|
518
|
+
|
|
514
519
|
// Try to find by opaque ID first, then fall back to path (for backwards compatibility)
|
|
515
520
|
const songPath = this.getSongPathFromId(songId);
|
|
516
|
-
const song = songPath
|
|
521
|
+
const song = songPath
|
|
517
522
|
? allSongs.find((s) => s.path === songPath)
|
|
518
523
|
: allSongs.find((s) => s.path === songId);
|
|
519
524
|
|
|
520
525
|
if (!song) {
|
|
521
|
-
|
|
526
|
+
log('❌ SONG NOT FOUND in library:', songId);
|
|
522
527
|
return res.status(404).json({ error: 'Song not found' });
|
|
523
528
|
}
|
|
524
529
|
|
|
525
|
-
|
|
530
|
+
log('✅ Song found:', song.title, 'by', song.artist);
|
|
526
531
|
|
|
527
532
|
// Generate opaque ID for the song if not already cached
|
|
528
533
|
const opaqueSongId = this.generateSongId(song.path);
|
|
529
|
-
|
|
534
|
+
|
|
530
535
|
const request = {
|
|
531
536
|
id: Date.now() + Math.random(),
|
|
532
537
|
songId: opaqueSongId, // Store opaque ID, not path
|
|
@@ -542,33 +547,33 @@ class WebServer {
|
|
|
542
547
|
clientIP: req.clientIP,
|
|
543
548
|
};
|
|
544
549
|
|
|
545
|
-
|
|
550
|
+
log('📝 Created request object:', request);
|
|
546
551
|
this.songRequests.push(request);
|
|
547
|
-
|
|
552
|
+
log('📋 Request added to list, total requests:', this.songRequests.length);
|
|
548
553
|
|
|
549
554
|
// If auto-approval is enabled, add to queue immediately
|
|
550
555
|
if (!this.settings.requireKJApproval) {
|
|
551
|
-
|
|
556
|
+
log('⚡ Auto-approval enabled, adding to queue...');
|
|
552
557
|
try {
|
|
553
558
|
await this.addToQueue(request);
|
|
554
559
|
request.status = 'queued';
|
|
555
|
-
|
|
560
|
+
log('✅ Successfully added to queue');
|
|
556
561
|
} catch (queueError) {
|
|
557
562
|
console.error('❌ Failed to add to queue:', queueError);
|
|
558
563
|
throw queueError;
|
|
559
564
|
}
|
|
560
565
|
} else {
|
|
561
|
-
|
|
566
|
+
log('⏳ Manual approval required, request pending');
|
|
562
567
|
}
|
|
563
568
|
|
|
564
569
|
// Notify the main app about the new request
|
|
565
|
-
|
|
570
|
+
log('📢 Notifying main app about new request...');
|
|
566
571
|
this.mainApp.onSongRequest?.(request);
|
|
567
572
|
|
|
568
573
|
// Broadcast to admin clients and renderer
|
|
569
574
|
this.io.to('admin-clients').emit('song-request', request);
|
|
570
575
|
this.io.to('electron-apps').emit('song-request', request);
|
|
571
|
-
|
|
576
|
+
log('📡 Broadcasted request to admin and renderer');
|
|
572
577
|
|
|
573
578
|
const responseData = {
|
|
574
579
|
success: true,
|
|
@@ -579,7 +584,7 @@ class WebServer {
|
|
|
579
584
|
status: request.status,
|
|
580
585
|
};
|
|
581
586
|
|
|
582
|
-
|
|
587
|
+
log('📤 Sending success response:', responseData);
|
|
583
588
|
res.json(responseData);
|
|
584
589
|
} catch (error) {
|
|
585
590
|
console.error('❌ ERROR processing request:', error);
|
|
@@ -660,11 +665,11 @@ class WebServer {
|
|
|
660
665
|
this.app.get('/api/butterchurn-screenshot/:presetName', (req, res) => {
|
|
661
666
|
const presetName = decodeURIComponent(req.params.presetName);
|
|
662
667
|
|
|
663
|
-
|
|
668
|
+
log(`Screenshot API request for: "${presetName}"`);
|
|
664
669
|
|
|
665
670
|
// Sanitize preset name same way as screenshot generator
|
|
666
671
|
const sanitizedName = presetName.replace(/[^a-zA-Z0-9-_\s]/g, '_') + '.png';
|
|
667
|
-
|
|
672
|
+
log(`Sanitized filename: "${sanitizedName}"`);
|
|
668
673
|
|
|
669
674
|
const screenshotsDir = path.join(__dirname, '../../static/images/butterchurn-screenshots');
|
|
670
675
|
|
|
@@ -708,23 +713,25 @@ class WebServer {
|
|
|
708
713
|
this.app.get('/api/state', (req, res) => {
|
|
709
714
|
try {
|
|
710
715
|
const state = this.mainApp.appState.getSnapshot();
|
|
711
|
-
|
|
716
|
+
|
|
712
717
|
// Sanitize for public consumption - only include safe info
|
|
713
718
|
const sanitizedState = {
|
|
714
719
|
// Current playback status (no paths)
|
|
715
|
-
currentSong: state.currentSong
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
720
|
+
currentSong: state.currentSong
|
|
721
|
+
? {
|
|
722
|
+
title: state.currentSong.title,
|
|
723
|
+
artist: state.currentSong.artist,
|
|
724
|
+
duration: state.currentSong.duration,
|
|
725
|
+
requester: state.currentSong.requester,
|
|
726
|
+
}
|
|
727
|
+
: null,
|
|
721
728
|
playback: {
|
|
722
729
|
isPlaying: state.playback?.isPlaying || false,
|
|
723
730
|
position: state.playback?.position || 0,
|
|
724
731
|
duration: state.playback?.duration || 0,
|
|
725
732
|
},
|
|
726
733
|
// Queue info (sanitized - no paths)
|
|
727
|
-
queue: (state.queue || []).map(item => ({
|
|
734
|
+
queue: (state.queue || []).map((item) => ({
|
|
728
735
|
id: item.id,
|
|
729
736
|
title: item.title,
|
|
730
737
|
artist: item.artist,
|
|
@@ -738,14 +745,14 @@ class WebServer {
|
|
|
738
745
|
},
|
|
739
746
|
// Exclude: mixer, effects, preferences, webServer config, paths, etc.
|
|
740
747
|
};
|
|
741
|
-
|
|
748
|
+
|
|
742
749
|
res.json(sanitizedState);
|
|
743
750
|
} catch (error) {
|
|
744
751
|
console.error('Error fetching app state:', error);
|
|
745
752
|
res.status(500).json({ error: 'Failed to fetch state' });
|
|
746
753
|
}
|
|
747
754
|
});
|
|
748
|
-
|
|
755
|
+
|
|
749
756
|
// Full state endpoint for admin - includes everything (behind auth via /admin/ prefix)
|
|
750
757
|
this.app.get('/admin/state-full', (req, res) => {
|
|
751
758
|
try {
|
|
@@ -1083,9 +1090,9 @@ class WebServer {
|
|
|
1083
1090
|
// For M4A files, add download URLs for extracted audio tracks
|
|
1084
1091
|
const audioFiles = result.kaiData.audio.sources.map((source) => {
|
|
1085
1092
|
const trackName = source.name;
|
|
1086
|
-
const fileId = Buffer.from(
|
|
1087
|
-
|
|
1088
|
-
);
|
|
1093
|
+
const fileId = Buffer.from(
|
|
1094
|
+
`${validatedPath}:${trackName}:${source.trackIndex}`
|
|
1095
|
+
).toString('base64url');
|
|
1089
1096
|
|
|
1090
1097
|
return {
|
|
1091
1098
|
name: source.name,
|
|
@@ -1197,7 +1204,7 @@ class WebServer {
|
|
|
1197
1204
|
const trackIndex = parseInt(trackIndexStr, 10);
|
|
1198
1205
|
const m4aPath = validation.resolvedPath;
|
|
1199
1206
|
|
|
1200
|
-
|
|
1207
|
+
log('📥 M4A audio request:', { m4aPath, trackName, trackIndex });
|
|
1201
1208
|
|
|
1202
1209
|
// Load the M4A file to extract the audio track
|
|
1203
1210
|
const M4ALoader = (await import('../utils/m4aLoader.js')).default;
|
|
@@ -1216,7 +1223,7 @@ class WebServer {
|
|
|
1216
1223
|
// Extract the audio track if not already extracted
|
|
1217
1224
|
let audioData = audioSource.audioData;
|
|
1218
1225
|
if (!audioData) {
|
|
1219
|
-
|
|
1226
|
+
log(`🎵 Extracting track ${trackIndex} from M4A file...`);
|
|
1220
1227
|
audioData = await M4ALoader.extractTrack(m4aPath, trackIndex);
|
|
1221
1228
|
}
|
|
1222
1229
|
|
|
@@ -1233,7 +1240,7 @@ class WebServer {
|
|
|
1233
1240
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
1234
1241
|
res.send(audioData);
|
|
1235
1242
|
|
|
1236
|
-
|
|
1243
|
+
log(`✅ Sent M4A track: ${filename} (${audioData.length} bytes)`);
|
|
1237
1244
|
} catch (error) {
|
|
1238
1245
|
console.error('Failed to download M4A audio:', error);
|
|
1239
1246
|
res.status(500).json({
|
|
@@ -1435,7 +1442,7 @@ class WebServer {
|
|
|
1435
1442
|
// Refresh library cache
|
|
1436
1443
|
this.app.post('/admin/library/refresh', async (req, res) => {
|
|
1437
1444
|
try {
|
|
1438
|
-
|
|
1445
|
+
log('🔄 Admin requested library cache refresh');
|
|
1439
1446
|
|
|
1440
1447
|
// Use libraryService to scan library
|
|
1441
1448
|
const result = await libraryService.scanLibrary(this.mainApp);
|
|
@@ -1921,11 +1928,11 @@ class WebServer {
|
|
|
1921
1928
|
// Get audio files that can be converted (from library or direct path)
|
|
1922
1929
|
this.app.get('/admin/creator/sources', async (req, res) => {
|
|
1923
1930
|
try {
|
|
1924
|
-
// Get library songs that are audio files (not already .stem.
|
|
1931
|
+
// Get library songs that are audio files (not already .stem.mp4)
|
|
1925
1932
|
const allSongs = await this.getCachedSongs();
|
|
1926
1933
|
|
|
1927
1934
|
// Filter to songs that could be source files for conversion
|
|
1928
|
-
// (exclude .stem.
|
|
1935
|
+
// (exclude .stem.mp4 which are already karaoke files)
|
|
1929
1936
|
const sourceCandidates = allSongs.filter((song) => {
|
|
1930
1937
|
const ext = song.path.split('.').pop().toLowerCase();
|
|
1931
1938
|
return [
|
|
@@ -2002,17 +2009,17 @@ class WebServer {
|
|
|
2002
2009
|
this.io.to('admin-clients').emit('playback-state-update', playbackState);
|
|
2003
2010
|
});
|
|
2004
2011
|
|
|
2005
|
-
|
|
2012
|
+
log('✅ State change listeners configured for WebSocket broadcasting');
|
|
2006
2013
|
}
|
|
2007
2014
|
|
|
2008
2015
|
setupSocketHandlers() {
|
|
2009
2016
|
this.io.on('connection', (socket) => {
|
|
2010
|
-
|
|
2017
|
+
log('Client connected:', socket.id);
|
|
2011
2018
|
|
|
2012
2019
|
// Handle connection type identification
|
|
2013
2020
|
socket.on('identify', (data) => {
|
|
2014
2021
|
socket.clientType = data.type; // 'electron-app', 'web-ui', or 'admin'
|
|
2015
|
-
|
|
2022
|
+
log(`Client identified as: ${data.type}`);
|
|
2016
2023
|
|
|
2017
2024
|
if (data.type === 'electron-app') {
|
|
2018
2025
|
socket.join('electron-apps');
|
|
@@ -2027,7 +2034,7 @@ class WebServer {
|
|
|
2027
2034
|
return;
|
|
2028
2035
|
}
|
|
2029
2036
|
socket.join('admin-clients');
|
|
2030
|
-
|
|
2037
|
+
log('Admin client connected and authenticated');
|
|
2031
2038
|
|
|
2032
2039
|
// Send current state to newly connected admin client
|
|
2033
2040
|
const currentState = this.mainApp.appState.getSnapshot();
|
|
@@ -2046,7 +2053,7 @@ class WebServer {
|
|
|
2046
2053
|
currentSong: currentState.currentSong,
|
|
2047
2054
|
});
|
|
2048
2055
|
socket.emit('playback-state-update', currentState.playback);
|
|
2049
|
-
|
|
2056
|
+
log('📤 Sent initial state to admin client:', {
|
|
2050
2057
|
mixer: currentState.mixer,
|
|
2051
2058
|
queue: currentState.queue.length,
|
|
2052
2059
|
playback: currentState.playback,
|
|
@@ -2057,7 +2064,7 @@ class WebServer {
|
|
|
2057
2064
|
|
|
2058
2065
|
// Handle disconnection
|
|
2059
2066
|
socket.on('disconnect', () => {
|
|
2060
|
-
|
|
2067
|
+
log('Client disconnected:', socket.id);
|
|
2061
2068
|
});
|
|
2062
2069
|
|
|
2063
2070
|
// Song request events
|
|
@@ -2102,13 +2109,13 @@ class WebServer {
|
|
|
2102
2109
|
socket.on('effect-control', (data) => {
|
|
2103
2110
|
// Forward effect control commands to electron apps
|
|
2104
2111
|
socket.to('electron-apps').emit('effect-control', data);
|
|
2105
|
-
|
|
2112
|
+
log(`Effect control: ${data.action}`);
|
|
2106
2113
|
});
|
|
2107
2114
|
});
|
|
2108
2115
|
}
|
|
2109
2116
|
|
|
2110
2117
|
async addToQueue(request) {
|
|
2111
|
-
|
|
2118
|
+
log('🎵 Adding to queue:', request.song.title);
|
|
2112
2119
|
|
|
2113
2120
|
// Add the song to the main app's queue
|
|
2114
2121
|
if (this.mainApp.addSongToQueue) {
|
|
@@ -2117,12 +2124,12 @@ class WebServer {
|
|
|
2117
2124
|
requester: request.requesterName,
|
|
2118
2125
|
addedVia: 'web-request',
|
|
2119
2126
|
};
|
|
2120
|
-
|
|
2121
|
-
|
|
2127
|
+
log('🎵 Queue item:', queueItem);
|
|
2128
|
+
log('🎵 Calling mainApp.addSongToQueue...');
|
|
2122
2129
|
|
|
2123
2130
|
try {
|
|
2124
2131
|
await this.mainApp.addSongToQueue(queueItem);
|
|
2125
|
-
|
|
2132
|
+
log('✅ Successfully called mainApp.addSongToQueue');
|
|
2126
2133
|
} catch (error) {
|
|
2127
2134
|
console.error('❌ Error in mainApp.addSongToQueue:', error);
|
|
2128
2135
|
throw error;
|
|
@@ -2166,11 +2173,11 @@ class WebServer {
|
|
|
2166
2173
|
if (!origin) {
|
|
2167
2174
|
return callback(null, true);
|
|
2168
2175
|
}
|
|
2169
|
-
|
|
2176
|
+
|
|
2170
2177
|
if (this.isAllowedOrigin(origin)) {
|
|
2171
2178
|
return callback(null, true);
|
|
2172
2179
|
}
|
|
2173
|
-
|
|
2180
|
+
|
|
2174
2181
|
callback(new Error('CORS not allowed for this origin'));
|
|
2175
2182
|
},
|
|
2176
2183
|
methods: ['GET', 'POST'],
|
|
@@ -2207,7 +2214,7 @@ class WebServer {
|
|
|
2207
2214
|
this.server = this.httpServer.listen(currentPort, (err) => {
|
|
2208
2215
|
if (err) {
|
|
2209
2216
|
if (err.code === 'EADDRINUSE' && currentPort < port + 10) {
|
|
2210
|
-
|
|
2217
|
+
log(`Port ${currentPort} in use, trying ${currentPort + 1}...`);
|
|
2211
2218
|
tryPort(currentPort + 1);
|
|
2212
2219
|
} else {
|
|
2213
2220
|
reject(err);
|
|
@@ -2218,9 +2225,9 @@ class WebServer {
|
|
|
2218
2225
|
// Load settings from persistent storage now that mainApp is available
|
|
2219
2226
|
this.settings = this.loadSettings();
|
|
2220
2227
|
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2228
|
+
log(`Web server started on http://localhost:${this.port}`);
|
|
2229
|
+
log(`Socket.IO server ready for connections`);
|
|
2230
|
+
log(`🔧 Loaded settings:`, this.settings);
|
|
2224
2231
|
resolve(this.port);
|
|
2225
2232
|
}
|
|
2226
2233
|
});
|
|
@@ -2247,7 +2254,7 @@ class WebServer {
|
|
|
2247
2254
|
if (this.server) {
|
|
2248
2255
|
this.server.close();
|
|
2249
2256
|
this.server = null;
|
|
2250
|
-
|
|
2257
|
+
log('Web server and Socket.IO server stopped');
|
|
2251
2258
|
}
|
|
2252
2259
|
|
|
2253
2260
|
if (this.httpServer) {
|
|
@@ -2328,7 +2335,7 @@ class WebServer {
|
|
|
2328
2335
|
}
|
|
2329
2336
|
|
|
2330
2337
|
return false;
|
|
2331
|
-
} catch
|
|
2338
|
+
} catch {
|
|
2332
2339
|
// Invalid URL - reject
|
|
2333
2340
|
return false;
|
|
2334
2341
|
}
|
|
@@ -2342,7 +2349,7 @@ class WebServer {
|
|
|
2342
2349
|
isPrivateIP(ip) {
|
|
2343
2350
|
// IPv4 private ranges
|
|
2344
2351
|
const parts = ip.split('.').map(Number);
|
|
2345
|
-
if (parts.length === 4 && parts.every(p => p >= 0 && p <= 255)) {
|
|
2352
|
+
if (parts.length === 4 && parts.every((p) => p >= 0 && p <= 255)) {
|
|
2346
2353
|
// 10.0.0.0/8
|
|
2347
2354
|
if (parts[0] === 10) return true;
|
|
2348
2355
|
// 172.16.0.0/12 (172.16.x.x - 172.31.x.x)
|
|
@@ -2437,7 +2444,7 @@ class WebServer {
|
|
|
2437
2444
|
// Get cached songs or refresh cache if needed
|
|
2438
2445
|
async getCachedSongs() {
|
|
2439
2446
|
if (!this.cachedSongs) {
|
|
2440
|
-
|
|
2447
|
+
log('📚 Loading songs into cache...');
|
|
2441
2448
|
await this.refreshSongsCache();
|
|
2442
2449
|
}
|
|
2443
2450
|
return this.cachedSongs;
|
|
@@ -2446,14 +2453,14 @@ class WebServer {
|
|
|
2446
2453
|
// Refresh the songs cache by scanning the directory
|
|
2447
2454
|
async refreshSongsCache() {
|
|
2448
2455
|
try {
|
|
2449
|
-
|
|
2456
|
+
log('🔄 Refreshing songs cache...');
|
|
2450
2457
|
this.cachedSongs = (await this.mainApp.getLibrarySongs?.()) || [];
|
|
2451
2458
|
this.songsCacheTime = Date.now();
|
|
2452
2459
|
|
|
2453
2460
|
// Reset Fuse.js instance since songs changed
|
|
2454
2461
|
this.fuse = null;
|
|
2455
2462
|
|
|
2456
|
-
|
|
2463
|
+
log(`✅ Cached ${this.cachedSongs.length} songs`);
|
|
2457
2464
|
} catch (error) {
|
|
2458
2465
|
console.error('❌ Failed to refresh songs cache:', error);
|
|
2459
2466
|
this.cachedSongs = [];
|
|
@@ -2462,7 +2469,7 @@ class WebServer {
|
|
|
2462
2469
|
|
|
2463
2470
|
// Clear the songs cache (useful for manual refresh)
|
|
2464
2471
|
clearSongsCache() {
|
|
2465
|
-
|
|
2472
|
+
log('🗑️ Clearing songs cache...');
|
|
2466
2473
|
this.cachedSongs = null;
|
|
2467
2474
|
this.songsCacheTime = null;
|
|
2468
2475
|
this.fuse = null;
|
|
@@ -2480,7 +2487,7 @@ class WebServer {
|
|
|
2480
2487
|
// Save it persistently
|
|
2481
2488
|
if (this.mainApp.settings) {
|
|
2482
2489
|
this.mainApp.settings.set(keyName, secretKey);
|
|
2483
|
-
|
|
2490
|
+
log('🔐 Generated new cookie encryption key');
|
|
2484
2491
|
}
|
|
2485
2492
|
}
|
|
2486
2493
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 2. Select audio file
|
|
7
7
|
* 3. Configure options (stems, whisper model, etc.)
|
|
8
8
|
* 4. Run conversion pipeline
|
|
9
|
-
* 5. Output .stem.
|
|
9
|
+
* 5. Output .stem.mp4 file
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
@@ -133,7 +133,7 @@ export function CreateTab({ bridge: _bridge }) {
|
|
|
133
133
|
const [options, setOptions] = useState({
|
|
134
134
|
title: '',
|
|
135
135
|
artist: '',
|
|
136
|
-
numStems: 4, // Always 4 stems for .stem.
|
|
136
|
+
numStems: 4, // Always 4 stems for .stem.mp4 format
|
|
137
137
|
language: 'en',
|
|
138
138
|
referenceLyrics: '',
|
|
139
139
|
});
|
|
@@ -391,9 +391,9 @@ export function CreateTab({ bridge: _bridge }) {
|
|
|
391
391
|
// Get output directory based on settings
|
|
392
392
|
let outputDir = undefined; // Default: same directory as source file
|
|
393
393
|
if (outputToSongsFolder) {
|
|
394
|
-
const
|
|
395
|
-
if (
|
|
396
|
-
outputDir =
|
|
394
|
+
const result = await window.kaiAPI?.library?.getSongsFolder?.();
|
|
395
|
+
if (result?.folder) {
|
|
396
|
+
outputDir = result.folder;
|
|
397
397
|
}
|
|
398
398
|
}
|
|
399
399
|
|
|
@@ -834,7 +834,7 @@ export function CreateTab({ bridge: _bridge }) {
|
|
|
834
834
|
</span>
|
|
835
835
|
</label>
|
|
836
836
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
837
|
-
When enabled, created .stem.
|
|
837
|
+
When enabled, created .stem.mp4 files will be saved to your configured songs library
|
|
838
838
|
folder instead of next to the source file.
|
|
839
839
|
</p>
|
|
840
840
|
</div>
|