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:
@@ -811,6 +811,8 @@
|
|||||||
|
|
||||||
<!-- Hidden Inputs -->
|
<!-- Hidden Inputs -->
|
||||||
<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"
|
||||||
|
onchange="performUpload('musicAudioFiles', '/api/upload/music-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"
|
||||||
@@ -1138,7 +1140,13 @@
|
|||||||
breadcrumb.innerHTML = '<div class="breadcrumb-item active">Home</div>';
|
breadcrumb.innerHTML = '<div class="breadcrumb-item active">Home</div>';
|
||||||
btnUpload.disabled = true;
|
btnUpload.disabled = true;
|
||||||
} else {
|
} 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 = `
|
breadcrumb.innerHTML = `
|
||||||
<div class="breadcrumb-item" onclick="navigate('root')">Home</div>
|
<div class="breadcrumb-item" onclick="navigate('root')">Home</div>
|
||||||
<div class="breadcrumb-sep">/</div>
|
<div class="breadcrumb-sep">/</div>
|
||||||
@@ -1151,8 +1159,9 @@
|
|||||||
if (currentPath === 'root') {
|
if (currentPath === 'root') {
|
||||||
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: 'general', name: 'General Audio', count: allFiles.filter(f => f.type === 'general').length },
|
{ id: 'music', name: 'Music', count: allFiles.filter(f => f.type === 'music').length },
|
||||||
{ id: 'station', name: 'Station Audio', count: allFiles.filter(f => f.type === 'station').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 => {
|
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>';
|
if (files.length === 0) container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">Empty folder</div>';
|
||||||
|
|
||||||
files.forEach(f => {
|
files.forEach(f => {
|
||||||
|
const typeLabelMap = {
|
||||||
|
config: 'Configuration',
|
||||||
|
music: 'Music',
|
||||||
|
beep: 'Beep',
|
||||||
|
station: 'Station'
|
||||||
|
};
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'file-row';
|
row.className = 'file-row';
|
||||||
// Click to preview
|
// Click to preview
|
||||||
@@ -1186,7 +1201,7 @@
|
|||||||
row.innerHTML = `
|
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;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 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">
|
<div style="text-align:right">
|
||||||
<button class="btn-icon" onclick="deleteFile('${f.path}')">${ICONS.trash}</button>
|
<button class="btn-icon" onclick="deleteFile('${f.path}')">${ICONS.trash}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1248,7 +1263,8 @@
|
|||||||
|
|
||||||
function triggerUpload() {
|
function triggerUpload() {
|
||||||
if (currentPath === 'config') document.getElementById('upload-config').click();
|
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();
|
else if (currentPath === 'station') document.getElementById('upload-station').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1347,8 +1363,16 @@
|
|||||||
|
|
||||||
function initAudioPlayer(path) {
|
function initAudioPlayer(path) {
|
||||||
if (audioInstance) { audioInstance.pause(); audioInstance = null; }
|
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;
|
audioInstance = audio;
|
||||||
|
|
||||||
const btn = document.getElementById('audio-play-btn');
|
const btn = document.getElementById('audio-play-btn');
|
||||||
|
|||||||
@@ -180,7 +180,7 @@
|
|||||||
<span class="badge" id="track_badge">未选择</span>
|
<span class="badge" id="track_badge">未选择</span>
|
||||||
</div>
|
</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">
|
<div class="row" style="margin-top:0">
|
||||||
<select id="track_select" onchange="onTrackChange()" style="flex:1; min-width: 200px;"></select>
|
<select id="track_select" onchange="onTrackChange()" style="flex:1; min-width: 200px;"></select>
|
||||||
<button class="btn" onclick="loadTracks()">刷新</button>
|
<button class="btn" onclick="loadTracks()">刷新</button>
|
||||||
@@ -316,7 +316,7 @@
|
|||||||
if (tracks.length === 0) {
|
if (tracks.length === 0) {
|
||||||
sel.innerHTML = '<option value="">(无可用音频)</option>';
|
sel.innerHTML = '<option value="">(无可用音频)</option>';
|
||||||
onTrackChange();
|
onTrackChange();
|
||||||
setStatus("没有可用音频(请上传到 /opt/CC-StationBroadcast/Audio/general)");
|
setStatus("没有可用音频(请上传到 /opt/CC-StationBroadcast/Audio/music)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sel.appendChild(new Option("请选择…", ""));
|
sel.appendChild(new Option("请选择…", ""));
|
||||||
@@ -339,7 +339,7 @@
|
|||||||
|
|
||||||
setStatus(`正在上传 ${file.name}…`);
|
setStatus(`正在上传 ${file.name}…`);
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/api/upload/general-audio", {
|
const resp = await fetch("/api/upload/music-audio", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|||||||
+59
-12
@@ -39,6 +39,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 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');
|
||||||
@@ -48,6 +49,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(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 });
|
||||||
@@ -57,6 +59,8 @@ const storage = multer.diskStorage({
|
|||||||
destination: function (req, file, cb) {
|
destination: function (req, file, cb) {
|
||||||
if (file.fieldname === 'listFile') {
|
if (file.fieldname === 'listFile') {
|
||||||
cb(null, appRootDir);
|
cb(null, appRootDir);
|
||||||
|
} else if (file.fieldname === 'musicAudioFiles') {
|
||||||
|
cb(null, audioMusicDir);
|
||||||
} 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') {
|
||||||
@@ -106,6 +110,10 @@ app.get('/media/audio/general/:file', (req, res) => {
|
|||||||
const name = path.basename(String(req.params.file || ''));
|
const name = path.basename(String(req.params.file || ''));
|
||||||
sendBinaryFile(res, path.join(audioGeneralDir, name));
|
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) => {
|
app.get('/media/audio/station/:file', (req, res) => {
|
||||||
const name = path.basename(String(req.params.file || ''));
|
const name = path.basename(String(req.params.file || ''));
|
||||||
@@ -122,6 +130,7 @@ app.get('/media', (req, res) => {
|
|||||||
success: true,
|
success: true,
|
||||||
message: 'Use a concrete media path',
|
message: 'Use a concrete media path',
|
||||||
routes: [
|
routes: [
|
||||||
|
'/media/audio/music/:file',
|
||||||
'/media/audio/general/:file',
|
'/media/audio/general/:file',
|
||||||
'/media/audio/station/:file',
|
'/media/audio/station/:file',
|
||||||
'/media/temp/:file'
|
'/media/temp/:file'
|
||||||
@@ -481,15 +490,21 @@ function getManagedMediaUrl(kind, fileName) {
|
|||||||
|
|
||||||
function listMusicTracks() {
|
function listMusicTracks() {
|
||||||
const out = new Set();
|
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)) {
|
for (const dir of scanDirs) {
|
||||||
if (!file || file.startsWith('.')) continue;
|
if (!fs.existsSync(dir)) continue;
|
||||||
const full = path.join(audioGeneralDir, file);
|
for (const file of fs.readdirSync(dir)) {
|
||||||
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) continue;
|
if (!file || file.startsWith('.')) continue;
|
||||||
if (!hasSupportedAudioExtension(file)) continue;
|
const full = path.join(dir, file);
|
||||||
const base = normalizeBaseName(file);
|
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) continue;
|
||||||
if (base) out.add(base);
|
if (!hasSupportedAudioExtension(file)) continue;
|
||||||
|
const base = normalizeBaseName(file);
|
||||||
|
if (base) out.add(base);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(out).sort((a, b) => a.localeCompare(b));
|
return Array.from(out).sort((a, b) => a.localeCompare(b));
|
||||||
@@ -533,6 +548,7 @@ function getPlaybackAudioByBaseName(baseName) {
|
|||||||
|
|
||||||
for (const ext of STREAMABLE_AUDIO_EXTENSIONS) {
|
for (const ext of STREAMABLE_AUDIO_EXTENSIONS) {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
|
path.join(audioMusicDir, `${base}${ext}`),
|
||||||
path.join(audioGeneralDir, `${base}${ext}`),
|
path.join(audioGeneralDir, `${base}${ext}`),
|
||||||
path.join(audioStationDir, `${base}${ext}`)
|
path.join(audioStationDir, `${base}${ext}`)
|
||||||
];
|
];
|
||||||
@@ -546,13 +562,18 @@ function getPlaybackAudioByBaseName(baseName) {
|
|||||||
return {
|
return {
|
||||||
sourcePath: p,
|
sourcePath: p,
|
||||||
audioFormat: 'stream',
|
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
|
durationSec
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const p of [
|
for (const p of [
|
||||||
|
path.join(audioMusicDir, `${base}.dfpwm`),
|
||||||
path.join(audioGeneralDir, `${base}.dfpwm`),
|
path.join(audioGeneralDir, `${base}.dfpwm`),
|
||||||
path.join(audioStationDir, `${base}.dfpwm`)
|
path.join(audioStationDir, `${base}.dfpwm`)
|
||||||
]) {
|
]) {
|
||||||
@@ -725,7 +746,9 @@ function resolveManagedPath(inputPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prefixes = [
|
const prefixes = [
|
||||||
|
{ prefix: 'Audio/music/', baseDir: audioMusicDir, logicalPrefix: 'Audio/music/' },
|
||||||
{ 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/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/' }
|
||||||
@@ -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)) {
|
if (fs.existsSync(audioGeneralDir)) {
|
||||||
fs.readdirSync(audioGeneralDir).forEach(file => {
|
fs.readdirSync(audioGeneralDir).forEach(file => {
|
||||||
if (file.endsWith('.dfpwm')) {
|
if (file.endsWith('.dfpwm')) {
|
||||||
@@ -769,7 +810,7 @@ function getManagedFiles() {
|
|||||||
files.push({
|
files.push({
|
||||||
name: file,
|
name: file,
|
||||||
path: `Audio/general/${file}`,
|
path: `Audio/general/${file}`,
|
||||||
type: 'general',
|
type: 'beep',
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
mtime: stats.mtimeMs
|
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) => {
|
app.post('/api/upload/general-audio', upload.array('generalAudioFiles'), (req, res) => {
|
||||||
const files = req.files || [];
|
const files = req.files || [];
|
||||||
log('SUCCESS', `Uploaded ${files.length} general audio files`);
|
log('SUCCESS', `Uploaded ${files.length} beep audio files`);
|
||||||
res.json({ success: true, message: `${files.length} general audio files uploaded` });
|
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) => {
|
app.post('/api/upload/station-audio', upload.array('stationAudioFiles'), (req, res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user