feat(console): add voice folder to file manager

Add an Audio/voice directory with upload and preview support, and expose it in the console file manager so the structure now shows Configuration, Music, Voice, Beep, and Station.
This commit is contained in:
2026-08-01 07:21:47 +08:00
parent 1f4be7c10b
commit fd53eaef97
2 changed files with 43 additions and 0 deletions
+8
View File
@@ -813,6 +813,8 @@
<input type="file" id="upload-config" accept=".txt" onchange="performUpload('listFile', '/api/upload/list', this)"> <input type="file" id="upload-config" accept=".txt" onchange="performUpload('listFile', '/api/upload/list', this)">
<input type="file" id="upload-music" multiple accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm" <input type="file" id="upload-music" multiple accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm"
onchange="performUpload('musicAudioFiles', '/api/upload/music-audio', this)"> onchange="performUpload('musicAudioFiles', '/api/upload/music-audio', this)">
<input type="file" id="upload-voice" multiple accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm"
onchange="performUpload('voiceAudioFiles', '/api/upload/voice-audio', this)">
<input type="file" id="upload-general" multiple accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm" <input type="file" id="upload-general" multiple accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm"
onchange="performUpload('generalAudioFiles', '/api/upload/general-audio', this)"> onchange="performUpload('generalAudioFiles', '/api/upload/general-audio', this)">
<input type="file" id="upload-station" multiple accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm" <input type="file" id="upload-station" multiple accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm"
@@ -1143,6 +1145,7 @@
const nameMap = { const nameMap = {
config: 'Configuration', config: 'Configuration',
music: 'Music', music: 'Music',
voice: 'Voice',
beep: 'Beep', beep: 'Beep',
station: 'Station' station: 'Station'
}; };
@@ -1160,6 +1163,7 @@
const folders = [ const folders = [
{ id: 'config', name: 'Configuration', count: allFiles.filter(f => f.type === 'config').length }, { id: 'config', name: 'Configuration', count: allFiles.filter(f => f.type === 'config').length },
{ id: 'music', name: 'Music', count: allFiles.filter(f => f.type === 'music').length }, { id: 'music', name: 'Music', count: allFiles.filter(f => f.type === 'music').length },
{ id: 'voice', name: 'Voice', count: allFiles.filter(f => f.type === 'voice').length },
{ id: 'beep', name: 'Beep', count: allFiles.filter(f => f.type === 'beep').length }, { id: 'beep', name: 'Beep', count: allFiles.filter(f => f.type === 'beep').length },
{ id: 'station', name: 'Station', count: allFiles.filter(f => f.type === 'station').length } { id: 'station', name: 'Station', count: allFiles.filter(f => f.type === 'station').length }
]; ];
@@ -1187,6 +1191,7 @@
const typeLabelMap = { const typeLabelMap = {
config: 'Configuration', config: 'Configuration',
music: 'Music', music: 'Music',
voice: 'Voice',
beep: 'Beep', beep: 'Beep',
station: 'Station' station: 'Station'
}; };
@@ -1264,6 +1269,7 @@
function triggerUpload() { function triggerUpload() {
if (currentPath === 'config') document.getElementById('upload-config').click(); if (currentPath === 'config') document.getElementById('upload-config').click();
else if (currentPath === 'music') document.getElementById('upload-music').click(); else if (currentPath === 'music') document.getElementById('upload-music').click();
else if (currentPath === 'voice') document.getElementById('upload-voice').click();
else if (currentPath === 'beep') document.getElementById('upload-general').click(); else if (currentPath === 'beep') document.getElementById('upload-general').click();
else if (currentPath === 'station') document.getElementById('upload-station').click(); else if (currentPath === 'station') document.getElementById('upload-station').click();
} }
@@ -1366,6 +1372,8 @@
let source = '/' + path; let source = '/' + path;
if (path.startsWith('Audio/music/')) { if (path.startsWith('Audio/music/')) {
source = `/media/audio/music/${encodeURIComponent(path.slice('Audio/music/'.length))}`; source = `/media/audio/music/${encodeURIComponent(path.slice('Audio/music/'.length))}`;
} else if (path.startsWith('Audio/voice/')) {
source = `/media/audio/voice/${encodeURIComponent(path.slice('Audio/voice/'.length))}`;
} else if (path.startsWith('Audio/general/')) { } else if (path.startsWith('Audio/general/')) {
source = `/media/audio/general/${encodeURIComponent(path.slice('Audio/general/'.length))}`; source = `/media/audio/general/${encodeURIComponent(path.slice('Audio/general/'.length))}`;
} else if (path.startsWith('Audio/station/')) { } else if (path.startsWith('Audio/station/')) {
+35
View File
@@ -40,6 +40,7 @@ if (!fs.existsSync(localHlsDir)) fs.mkdirSync(localHlsDir, { recursive: true });
const uploadDir = path.join(__dirname, 'public', 'uploads'); const uploadDir = path.join(__dirname, 'public', 'uploads');
const audioRootDir = path.join(appRootDir, 'Audio'); const audioRootDir = path.join(appRootDir, 'Audio');
const audioMusicDir = path.join(audioRootDir, 'music'); const audioMusicDir = path.join(audioRootDir, 'music');
const audioVoiceDir = path.join(audioRootDir, 'voice');
const audioGeneralDir = path.join(audioRootDir, 'general'); const audioGeneralDir = path.join(audioRootDir, 'general');
const audioStationDir = path.join(audioRootDir, 'station'); const audioStationDir = path.join(audioRootDir, 'station');
const listFilePath = path.join(appRootDir, 'list.txt'); const listFilePath = path.join(appRootDir, 'list.txt');
@@ -50,6 +51,7 @@ const MUSIC_AUDIO_EXTENSIONS = [...STREAMABLE_AUDIO_EXTENSIONS, '.dfpwm'];
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
if (!fs.existsSync(audioRootDir)) fs.mkdirSync(audioRootDir, { recursive: true }); if (!fs.existsSync(audioRootDir)) fs.mkdirSync(audioRootDir, { recursive: true });
if (!fs.existsSync(audioMusicDir)) fs.mkdirSync(audioMusicDir, { recursive: true }); if (!fs.existsSync(audioMusicDir)) fs.mkdirSync(audioMusicDir, { recursive: true });
if (!fs.existsSync(audioVoiceDir)) fs.mkdirSync(audioVoiceDir, { recursive: true });
if (!fs.existsSync(audioGeneralDir)) fs.mkdirSync(audioGeneralDir, { recursive: true }); if (!fs.existsSync(audioGeneralDir)) fs.mkdirSync(audioGeneralDir, { recursive: true });
if (!fs.existsSync(audioStationDir)) fs.mkdirSync(audioStationDir, { recursive: true }); if (!fs.existsSync(audioStationDir)) fs.mkdirSync(audioStationDir, { recursive: true });
if (!fs.existsSync(tempBroadcastDir)) fs.mkdirSync(tempBroadcastDir, { recursive: true }); if (!fs.existsSync(tempBroadcastDir)) fs.mkdirSync(tempBroadcastDir, { recursive: true });
@@ -61,6 +63,8 @@ const storage = multer.diskStorage({
cb(null, appRootDir); cb(null, appRootDir);
} else if (file.fieldname === 'musicAudioFiles') { } else if (file.fieldname === 'musicAudioFiles') {
cb(null, audioMusicDir); cb(null, audioMusicDir);
} else if (file.fieldname === 'voiceAudioFiles') {
cb(null, audioVoiceDir);
} else if (file.fieldname === 'generalAudioFiles') { } else if (file.fieldname === 'generalAudioFiles') {
cb(null, audioGeneralDir); cb(null, audioGeneralDir);
} else if (file.fieldname === 'stationAudioFiles') { } else if (file.fieldname === 'stationAudioFiles') {
@@ -114,6 +118,10 @@ app.get('/media/audio/music/:file', (req, res) => {
const name = path.basename(String(req.params.file || '')); const name = path.basename(String(req.params.file || ''));
sendBinaryFile(res, path.join(audioMusicDir, name)); sendBinaryFile(res, path.join(audioMusicDir, name));
}); });
app.get('/media/audio/voice/:file', (req, res) => {
const name = path.basename(String(req.params.file || ''));
sendBinaryFile(res, path.join(audioVoiceDir, name));
});
app.get('/media/audio/station/:file', (req, res) => { app.get('/media/audio/station/:file', (req, res) => {
const name = path.basename(String(req.params.file || '')); const name = path.basename(String(req.params.file || ''));
@@ -131,6 +139,7 @@ app.get('/media', (req, res) => {
message: 'Use a concrete media path', message: 'Use a concrete media path',
routes: [ routes: [
'/media/audio/music/:file', '/media/audio/music/:file',
'/media/audio/voice/:file',
'/media/audio/general/:file', '/media/audio/general/:file',
'/media/audio/station/:file', '/media/audio/station/:file',
'/media/temp/:file' '/media/temp/:file'
@@ -747,8 +756,10 @@ function resolveManagedPath(inputPath) {
const prefixes = [ const prefixes = [
{ prefix: 'Audio/music/', baseDir: audioMusicDir, logicalPrefix: 'Audio/music/' }, { prefix: 'Audio/music/', baseDir: audioMusicDir, logicalPrefix: 'Audio/music/' },
{ prefix: 'Audio/voice/', baseDir: audioVoiceDir, logicalPrefix: 'Audio/voice/' },
{ prefix: 'Audio/general/', baseDir: audioGeneralDir, logicalPrefix: 'Audio/general/' }, { prefix: 'Audio/general/', baseDir: audioGeneralDir, logicalPrefix: 'Audio/general/' },
{ prefix: 'uploads/audio/music/', baseDir: audioMusicDir, logicalPrefix: 'Audio/music/' }, { prefix: 'uploads/audio/music/', baseDir: audioMusicDir, logicalPrefix: 'Audio/music/' },
{ prefix: 'uploads/audio/voice/', baseDir: audioVoiceDir, logicalPrefix: 'Audio/voice/' },
{ prefix: 'uploads/audio/general/', baseDir: audioGeneralDir, logicalPrefix: 'Audio/general/' }, { prefix: 'uploads/audio/general/', baseDir: audioGeneralDir, logicalPrefix: 'Audio/general/' },
{ prefix: 'Audio/station/', baseDir: audioStationDir, logicalPrefix: 'Audio/station/' }, { prefix: 'Audio/station/', baseDir: audioStationDir, logicalPrefix: 'Audio/station/' },
{ prefix: 'uploads/audio/station/', baseDir: audioStationDir, logicalPrefix: 'Audio/station/' } { prefix: 'uploads/audio/station/', baseDir: audioStationDir, logicalPrefix: 'Audio/station/' }
@@ -799,6 +810,24 @@ function getManagedFiles() {
}); });
} }
if (fs.existsSync(audioVoiceDir)) {
fs.readdirSync(audioVoiceDir).forEach(file => {
if (file.endsWith('.dfpwm')) {
const wavName = file.replace(/\.dfpwm$/, '.wav');
if (fs.existsSync(path.join(audioVoiceDir, wavName))) return;
}
const filePath = path.join(audioVoiceDir, file);
const stats = fs.statSync(filePath);
files.push({
name: file,
path: `Audio/voice/${file}`,
type: 'voice',
size: stats.size,
mtime: stats.mtimeMs
});
});
}
if (fs.existsSync(audioGeneralDir)) { if (fs.existsSync(audioGeneralDir)) {
fs.readdirSync(audioGeneralDir).forEach(file => { fs.readdirSync(audioGeneralDir).forEach(file => {
if (file.endsWith('.dfpwm')) { if (file.endsWith('.dfpwm')) {
@@ -1898,6 +1927,12 @@ app.post('/api/upload/music-audio', upload.array('musicAudioFiles'), (req, res)
res.json({ success: true, message: `${files.length} music audio files uploaded` }); res.json({ success: true, message: `${files.length} music audio files uploaded` });
}); });
app.post('/api/upload/voice-audio', upload.array('voiceAudioFiles'), (req, res) => {
const files = req.files || [];
log('SUCCESS', `Uploaded ${files.length} voice audio files`);
res.json({ success: true, message: `${files.length} voice audio files uploaded` });
});
app.post('/api/upload/station-audio', upload.array('stationAudioFiles'), (req, res) => { app.post('/api/upload/station-audio', upload.array('stationAudioFiles'), (req, res) => {
const files = req.files || []; const files = req.files || [];
log('SUCCESS', `Uploaded ${files.length} station audio files`); log('SUCCESS', `Uploaded ${files.length} station audio files`);