feat(console): split audio files into music and beep folders

Add an Audio/music path for uploaded music, keep the existing general audio directory as the Beep group, and update the console and music page so the file manager now shows Configuration, Music, Beep, and Station with matching upload and preview routes.
This commit is contained in:
2026-08-01 07:18:14 +08:00
parent 1554adf5f1
commit 1f4be7c10b
3 changed files with 92 additions and 21 deletions
+30 -6
View File
@@ -811,6 +811,8 @@
<!-- Hidden Inputs -->
<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"
onchange="performUpload('musicAudioFiles', '/api/upload/music-audio', this)">
<input type="file" id="upload-general" multiple accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm"
onchange="performUpload('generalAudioFiles', '/api/upload/general-audio', this)">
<input type="file" id="upload-station" multiple accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm"
@@ -1138,7 +1140,13 @@
breadcrumb.innerHTML = '<div class="breadcrumb-item active">Home</div>';
btnUpload.disabled = true;
} else {
const name = currentPath === 'config' ? 'Config' : (currentPath === 'general' ? 'General' : 'Station');
const nameMap = {
config: 'Configuration',
music: 'Music',
beep: 'Beep',
station: 'Station'
};
const name = nameMap[currentPath] || currentPath;
breadcrumb.innerHTML = `
<div class="breadcrumb-item" onclick="navigate('root')">Home</div>
<div class="breadcrumb-sep">/</div>
@@ -1151,8 +1159,9 @@
if (currentPath === 'root') {
const folders = [
{ id: 'config', name: 'Configuration', count: allFiles.filter(f => f.type === 'config').length },
{ id: 'general', name: 'General Audio', count: allFiles.filter(f => f.type === 'general').length },
{ id: 'station', name: 'Station Audio', count: allFiles.filter(f => f.type === 'station').length }
{ id: 'music', name: 'Music', count: allFiles.filter(f => f.type === 'music').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 }
];
folders.forEach(f => {
@@ -1175,6 +1184,12 @@
if (files.length === 0) container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">Empty folder</div>';
files.forEach(f => {
const typeLabelMap = {
config: 'Configuration',
music: 'Music',
beep: 'Beep',
station: 'Station'
};
const row = document.createElement('div');
row.className = 'file-row';
// Click to preview
@@ -1186,7 +1201,7 @@
row.innerHTML = `
<div style="display:flex;justify-content:center"><input type="checkbox" class="file-check" value="${f.path}" onchange="updateSelection()"></div>
<div style="display:flex;align-items:center;gap:10px">${f.name.endsWith('.txt') ? ICONS.file : ICONS.audio}<span class="file-name">${f.name}</span></div>
<div class="file-meta">${f.type}</div>
<div class="file-meta">${typeLabelMap[f.type] || f.type}</div>
<div style="text-align:right">
<button class="btn-icon" onclick="deleteFile('${f.path}')">${ICONS.trash}</button>
</div>
@@ -1248,7 +1263,8 @@
function triggerUpload() {
if (currentPath === 'config') document.getElementById('upload-config').click();
else if (currentPath === 'general') document.getElementById('upload-general').click();
else if (currentPath === 'music') document.getElementById('upload-music').click();
else if (currentPath === 'beep') document.getElementById('upload-general').click();
else if (currentPath === 'station') document.getElementById('upload-station').click();
}
@@ -1347,8 +1363,16 @@
function initAudioPlayer(path) {
if (audioInstance) { audioInstance.pause(); audioInstance = null; }
let source = '/' + path;
if (path.startsWith('Audio/music/')) {
source = `/media/audio/music/${encodeURIComponent(path.slice('Audio/music/'.length))}`;
} else if (path.startsWith('Audio/general/')) {
source = `/media/audio/general/${encodeURIComponent(path.slice('Audio/general/'.length))}`;
} else if (path.startsWith('Audio/station/')) {
source = `/media/audio/station/${encodeURIComponent(path.slice('Audio/station/'.length))}`;
}
const audio = new Audio('/' + path);
const audio = new Audio(source);
audioInstance = audio;
const btn = document.getElementById('audio-play-btn');
+3 -3
View File
@@ -180,7 +180,7 @@
<span class="badge" id="track_badge">未选择</span>
</div>
<label for="track_select">选择音乐(来自 /opt/CC-StationBroadcast/Audio/general)</label>
<label for="track_select">选择音乐(来自 /opt/CC-StationBroadcast/Audio/music)</label>
<div class="row" style="margin-top:0">
<select id="track_select" onchange="onTrackChange()" style="flex:1; min-width: 200px;"></select>
<button class="btn" onclick="loadTracks()">刷新</button>
@@ -316,7 +316,7 @@
if (tracks.length === 0) {
sel.innerHTML = '<option value="">(无可用音频)</option>';
onTrackChange();
setStatus("没有可用音频(请上传到 /opt/CC-StationBroadcast/Audio/general)");
setStatus("没有可用音频(请上传到 /opt/CC-StationBroadcast/Audio/music)");
return;
}
sel.appendChild(new Option("请选择…", ""));
@@ -339,7 +339,7 @@
setStatus(`正在上传 ${file.name}…`);
try {
const resp = await fetch("/api/upload/general-audio", {
const resp = await fetch("/api/upload/music-audio", {
method: "POST",
body: formData
});
+59 -12
View File
@@ -39,6 +39,7 @@ if (!fs.existsSync(localHlsDir)) fs.mkdirSync(localHlsDir, { recursive: true });
const uploadDir = path.join(__dirname, 'public', 'uploads');
const audioRootDir = path.join(appRootDir, 'Audio');
const audioMusicDir = path.join(audioRootDir, 'music');
const audioGeneralDir = path.join(audioRootDir, 'general');
const audioStationDir = path.join(audioRootDir, 'station');
const listFilePath = path.join(appRootDir, 'list.txt');
@@ -48,6 +49,7 @@ const MUSIC_AUDIO_EXTENSIONS = [...STREAMABLE_AUDIO_EXTENSIONS, '.dfpwm'];
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
if (!fs.existsSync(audioRootDir)) fs.mkdirSync(audioRootDir, { recursive: true });
if (!fs.existsSync(audioMusicDir)) fs.mkdirSync(audioMusicDir, { recursive: true });
if (!fs.existsSync(audioGeneralDir)) fs.mkdirSync(audioGeneralDir, { recursive: true });
if (!fs.existsSync(audioStationDir)) fs.mkdirSync(audioStationDir, { recursive: true });
if (!fs.existsSync(tempBroadcastDir)) fs.mkdirSync(tempBroadcastDir, { recursive: true });
@@ -57,6 +59,8 @@ const storage = multer.diskStorage({
destination: function (req, file, cb) {
if (file.fieldname === 'listFile') {
cb(null, appRootDir);
} else if (file.fieldname === 'musicAudioFiles') {
cb(null, audioMusicDir);
} else if (file.fieldname === 'generalAudioFiles') {
cb(null, audioGeneralDir);
} else if (file.fieldname === 'stationAudioFiles') {
@@ -106,6 +110,10 @@ app.get('/media/audio/general/:file', (req, res) => {
const name = path.basename(String(req.params.file || ''));
sendBinaryFile(res, path.join(audioGeneralDir, name));
});
app.get('/media/audio/music/:file', (req, res) => {
const name = path.basename(String(req.params.file || ''));
sendBinaryFile(res, path.join(audioMusicDir, name));
});
app.get('/media/audio/station/:file', (req, res) => {
const name = path.basename(String(req.params.file || ''));
@@ -122,6 +130,7 @@ app.get('/media', (req, res) => {
success: true,
message: 'Use a concrete media path',
routes: [
'/media/audio/music/:file',
'/media/audio/general/:file',
'/media/audio/station/:file',
'/media/temp/:file'
@@ -481,15 +490,21 @@ function getManagedMediaUrl(kind, fileName) {
function listMusicTracks() {
const out = new Set();
if (!fs.existsSync(audioGeneralDir)) return [];
const scanDirs = [audioMusicDir];
if (fs.existsSync(audioMusicDir) && fs.readdirSync(audioMusicDir).length === 0) {
scanDirs.push(audioGeneralDir);
}
for (const file of fs.readdirSync(audioGeneralDir)) {
if (!file || file.startsWith('.')) continue;
const full = path.join(audioGeneralDir, file);
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) continue;
if (!hasSupportedAudioExtension(file)) continue;
const base = normalizeBaseName(file);
if (base) out.add(base);
for (const dir of scanDirs) {
if (!fs.existsSync(dir)) continue;
for (const file of fs.readdirSync(dir)) {
if (!file || file.startsWith('.')) continue;
const full = path.join(dir, file);
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) continue;
if (!hasSupportedAudioExtension(file)) continue;
const base = normalizeBaseName(file);
if (base) out.add(base);
}
}
return Array.from(out).sort((a, b) => a.localeCompare(b));
@@ -533,6 +548,7 @@ function getPlaybackAudioByBaseName(baseName) {
for (const ext of STREAMABLE_AUDIO_EXTENSIONS) {
const candidates = [
path.join(audioMusicDir, `${base}${ext}`),
path.join(audioGeneralDir, `${base}${ext}`),
path.join(audioStationDir, `${base}${ext}`)
];
@@ -546,13 +562,18 @@ function getPlaybackAudioByBaseName(baseName) {
return {
sourcePath: p,
audioFormat: 'stream',
streamUrl: getManagedMediaUrl(p.startsWith(audioStationDir) ? 'audio/station' : 'audio/general', path.basename(p)),
streamUrl: getManagedMediaUrl(
p.startsWith(audioStationDir) ? 'audio/station' :
(p.startsWith(audioMusicDir) ? 'audio/music' : 'audio/general'),
path.basename(p)
),
durationSec
};
}
}
for (const p of [
path.join(audioMusicDir, `${base}.dfpwm`),
path.join(audioGeneralDir, `${base}.dfpwm`),
path.join(audioStationDir, `${base}.dfpwm`)
]) {
@@ -725,7 +746,9 @@ function resolveManagedPath(inputPath) {
}
const prefixes = [
{ prefix: 'Audio/music/', baseDir: audioMusicDir, logicalPrefix: 'Audio/music/' },
{ prefix: 'Audio/general/', baseDir: audioGeneralDir, logicalPrefix: 'Audio/general/' },
{ prefix: 'uploads/audio/music/', baseDir: audioMusicDir, logicalPrefix: 'Audio/music/' },
{ prefix: 'uploads/audio/general/', baseDir: audioGeneralDir, logicalPrefix: 'Audio/general/' },
{ prefix: 'Audio/station/', baseDir: audioStationDir, logicalPrefix: 'Audio/station/' },
{ prefix: 'uploads/audio/station/', baseDir: audioStationDir, logicalPrefix: 'Audio/station/' }
@@ -758,6 +781,24 @@ function getManagedFiles() {
});
}
if (fs.existsSync(audioMusicDir)) {
fs.readdirSync(audioMusicDir).forEach(file => {
if (file.endsWith('.dfpwm')) {
const wavName = file.replace(/\.dfpwm$/, '.wav');
if (fs.existsSync(path.join(audioMusicDir, wavName))) return;
}
const filePath = path.join(audioMusicDir, file);
const stats = fs.statSync(filePath);
files.push({
name: file,
path: `Audio/music/${file}`,
type: 'music',
size: stats.size,
mtime: stats.mtimeMs
});
});
}
if (fs.existsSync(audioGeneralDir)) {
fs.readdirSync(audioGeneralDir).forEach(file => {
if (file.endsWith('.dfpwm')) {
@@ -769,7 +810,7 @@ function getManagedFiles() {
files.push({
name: file,
path: `Audio/general/${file}`,
type: 'general',
type: 'beep',
size: stats.size,
mtime: stats.mtimeMs
});
@@ -1847,8 +1888,14 @@ app.post('/api/upload/list', upload.single('listFile'), (req, res) => {
app.post('/api/upload/general-audio', upload.array('generalAudioFiles'), (req, res) => {
const files = req.files || [];
log('SUCCESS', `Uploaded ${files.length} general audio files`);
res.json({ success: true, message: `${files.length} general audio files uploaded` });
log('SUCCESS', `Uploaded ${files.length} beep audio files`);
res.json({ success: true, message: `${files.length} beep audio files uploaded` });
});
app.post('/api/upload/music-audio', upload.array('musicAudioFiles'), (req, res) => {
const files = req.files || [];
log('SUCCESS', `Uploaded ${files.length} music audio files`);
res.json({ success: true, message: `${files.length} music audio files uploaded` });
});
app.post('/api/upload/station-audio', upload.array('stationAudioFiles'), (req, res) => {