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) => {