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.
Files changed (49) hide show
  1. package/README.md +5 -5
  2. package/package.json +2 -2
  3. package/src/main/audioEngine.js +11 -10
  4. package/src/main/creator/conversionService.js +9 -8
  5. package/src/main/creator/downloadManager.js +70 -67
  6. package/src/main/creator/ffmpegService.js +3 -2
  7. package/src/main/creator/installLogger.js +5 -7
  8. package/src/main/creator/llmService.js +13 -7
  9. package/src/main/creator/lrclibService.js +5 -6
  10. package/src/main/creator/pythonRunner.js +3 -2
  11. package/src/main/creator/stemBuilder.js +29 -28
  12. package/src/main/handlers/audioHandlers.js +2 -1
  13. package/src/main/handlers/canvasHandlers.js +4 -3
  14. package/src/main/handlers/creatorHandlers.js +2 -1
  15. package/src/main/handlers/editorHandlers.js +8 -7
  16. package/src/main/handlers/fileHandlers.js +1 -1
  17. package/src/main/handlers/index.js +21 -19
  18. package/src/main/handlers/libraryHandlers.js +3 -0
  19. package/src/main/handlers/queueHandlers.js +3 -2
  20. package/src/main/handlers/settingsHandlers.js +2 -1
  21. package/src/main/handlers/webServerHandlers.js +3 -2
  22. package/src/main/logger.js +12 -0
  23. package/src/main/main.js +71 -66
  24. package/src/main/statePersistence.js +13 -12
  25. package/src/main/utils/pathValidator.js +21 -17
  26. package/src/main/webServer.js +102 -95
  27. package/src/renderer/components/creator/CreateTab.jsx +6 -6
  28. package/src/renderer/dist/assets/{kaiPlayer-CoMx__a_.js → kaiPlayer-DSaY7TxC.js} +2 -2
  29. package/src/renderer/dist/assets/kaiPlayer-DSaY7TxC.js.map +1 -0
  30. package/src/renderer/dist/assets/songLoaders-CcYVonLu.js +2 -0
  31. package/src/renderer/dist/assets/{songLoaders-BaTgGib4.js.map → songLoaders-CcYVonLu.js.map} +1 -1
  32. package/src/renderer/dist/renderer.css +1 -1
  33. package/src/renderer/dist/renderer.js +11 -51
  34. package/src/renderer/dist/renderer.js.map +1 -1
  35. package/src/renderer/js/kaiPlayer.js +9 -7
  36. package/src/renderer/lib/cdgraphics.js +0 -1
  37. package/src/shared/services/creatorService.js +2 -2
  38. package/src/shared/services/libraryService.js +4 -1
  39. package/src/shared/services/serverSettingsService.js +0 -1
  40. package/src/shared/services/settingsService.js +0 -2
  41. package/src/utils/m4aLoader.js +1 -1
  42. package/src/web/dist/assets/index-CGbmW1VG.js +11 -0
  43. package/src/web/dist/assets/{index-0H-RnRrV.js.map → index-CGbmW1VG.js.map} +1 -1
  44. package/src/web/dist/assets/index-GLKJK41r.css +1 -0
  45. package/src/web/dist/index.html +2 -2
  46. package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +0 -1
  47. package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +0 -2
  48. package/src/web/dist/assets/index-0H-RnRrV.js +0 -51
  49. package/src/web/dist/assets/index-DYW2zB0u.css +0 -1
@@ -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(cors({
132
- origin: (origin, callback) => {
133
- // Allow requests with no origin (same-origin, non-browser clients, curl, etc.)
134
- if (!origin) {
135
- return callback(null, true);
136
- }
137
-
138
- if (this.isAllowedOrigin(origin)) {
139
- return callback(null, true);
140
- }
141
-
142
- // Reject other origins
143
- callback(new Error('CORS not allowed for this origin'));
144
- },
145
- credentials: true,
146
- methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
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
- console.log('API: Getting available letters...');
319
+ log('API: Getting available letters...');
317
320
 
318
321
  // Get songs from cache
319
322
  const allSongs = await this.getCachedSongs();
320
- console.log(`API: Found ${allSongs.length} songs`);
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
- console.log(`API: Getting songs for letter ${letter}, page ${page}, limit ${limit}`);
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
- console.log('API: Getting songs from cache...');
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
- console.log(`API: Found ${allSongs.length} songs`);
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.slice(0, limit).map((result) => this.sanitizeSongForPublic(result.item));
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
- console.log('🎤 NEW REQUEST received:', req.body);
496
+ log('🎤 NEW REQUEST received:', req.body);
492
497
 
493
498
  if (!this.settings.allowSongRequests) {
494
- console.log('❌ REQUEST DENIED: requests disabled');
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
- console.log('❌ REQUEST DENIED: missing required fields', {
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
- console.log('🔍 Looking for song with ID:', songId);
515
+ log('🔍 Looking for song with ID:', songId);
511
516
  const allSongs = await this.getCachedSongs();
512
- console.log('📚 Found library with', allSongs.length, 'songs');
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
- console.log('❌ SONG NOT FOUND in library:', songId);
526
+ log('❌ SONG NOT FOUND in library:', songId);
522
527
  return res.status(404).json({ error: 'Song not found' });
523
528
  }
524
529
 
525
- console.log('✅ Song found:', song.title, 'by', song.artist);
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
- console.log('📝 Created request object:', request);
550
+ log('📝 Created request object:', request);
546
551
  this.songRequests.push(request);
547
- console.log('📋 Request added to list, total requests:', this.songRequests.length);
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
- console.log('⚡ Auto-approval enabled, adding to queue...');
556
+ log('⚡ Auto-approval enabled, adding to queue...');
552
557
  try {
553
558
  await this.addToQueue(request);
554
559
  request.status = 'queued';
555
- console.log('✅ Successfully added to queue');
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
- console.log('⏳ Manual approval required, request pending');
566
+ log('⏳ Manual approval required, request pending');
562
567
  }
563
568
 
564
569
  // Notify the main app about the new request
565
- console.log('📢 Notifying main app about new request...');
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
- console.log('📡 Broadcasted request to admin and renderer');
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
- console.log('📤 Sending success response:', responseData);
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
- console.log(`Screenshot API request for: "${presetName}"`);
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
- console.log(`Sanitized filename: "${sanitizedName}"`);
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
- title: state.currentSong.title,
717
- artist: state.currentSong.artist,
718
- duration: state.currentSong.duration,
719
- requester: state.currentSong.requester,
720
- } : null,
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(`${validatedPath}:${trackName}:${source.trackIndex}`).toString(
1087
- 'base64url'
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
- console.log('📥 M4A audio request:', { m4aPath, trackName, trackIndex });
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
- console.log(`🎵 Extracting track ${trackIndex} from M4A file...`);
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
- console.log(`✅ Sent M4A track: ${filename} (${audioData.length} bytes)`);
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
- console.log('🔄 Admin requested library cache refresh');
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.m4a)
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.m4a which are already karaoke files)
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
- console.log('✅ State change listeners configured for WebSocket broadcasting');
2012
+ log('✅ State change listeners configured for WebSocket broadcasting');
2006
2013
  }
2007
2014
 
2008
2015
  setupSocketHandlers() {
2009
2016
  this.io.on('connection', (socket) => {
2010
- console.log('Client connected:', socket.id);
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
- console.log(`Client identified as: ${data.type}`);
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
- console.log('Admin client connected and authenticated');
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
- console.log('📤 Sent initial state to admin client:', {
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
- console.log('Client disconnected:', socket.id);
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
- console.log(`Effect control: ${data.action}`);
2112
+ log(`Effect control: ${data.action}`);
2106
2113
  });
2107
2114
  });
2108
2115
  }
2109
2116
 
2110
2117
  async addToQueue(request) {
2111
- console.log('🎵 Adding to queue:', request.song.title);
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
- console.log('🎵 Queue item:', queueItem);
2121
- console.log('🎵 Calling mainApp.addSongToQueue...');
2127
+ log('🎵 Queue item:', queueItem);
2128
+ log('🎵 Calling mainApp.addSongToQueue...');
2122
2129
 
2123
2130
  try {
2124
2131
  await this.mainApp.addSongToQueue(queueItem);
2125
- console.log('✅ Successfully called mainApp.addSongToQueue');
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
- console.log(`Port ${currentPort} in use, trying ${currentPort + 1}...`);
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
- console.log(`Web server started on http://localhost:${this.port}`);
2222
- console.log(`Socket.IO server ready for connections`);
2223
- console.log(`🔧 Loaded settings:`, this.settings);
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
- console.log('Web server and Socket.IO server stopped');
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 (error) {
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
- console.log('📚 Loading songs into cache...');
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
- console.log('🔄 Refreshing songs cache...');
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
- console.log(`✅ Cached ${this.cachedSongs.length} songs`);
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
- console.log('🗑️ Clearing songs cache...');
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
- console.log('🔐 Generated new cookie encryption key');
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.m4a file
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.m4a format
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 songsFolder = await window.kaiAPI?.library?.getSongsFolder?.();
395
- if (songsFolder) {
396
- outputDir = songsFolder;
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.m4a files will be saved to your configured songs library
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>