diff --git a/CloudServer/public/index.html b/CloudServer/public/index.html
index e0db4d4..b271f12 100644
--- a/CloudServer/public/index.html
+++ b/CloudServer/public/index.html
@@ -811,6 +811,8 @@
+
Home
/
@@ -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 = 'Empty folder
';
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 = `
${f.name.endsWith('.txt') ? ICONS.file : ICONS.audio}${f.name}
- ${f.type}
+ ${typeLabelMap[f.type] || f.type}
@@ -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');
diff --git a/CloudServer/public/music.html b/CloudServer/public/music.html
index fbb3b70..f7d4e34 100644
--- a/CloudServer/public/music.html
+++ b/CloudServer/public/music.html
@@ -180,7 +180,7 @@
未选择
-
+
@@ -316,7 +316,7 @@
if (tracks.length === 0) {
sel.innerHTML = '';
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
});
diff --git a/CloudServer/server.js b/CloudServer/server.js
index cd40f7d..3b7aaac 100644
--- a/CloudServer/server.js
+++ b/CloudServer/server.js
@@ -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) => {