feat(audio): support more upload and music formats
Allow the console and music page to upload common audio formats, expand music track detection and playback source selection beyond wav/dfpwm, and update station list name normalization so common extensions resolve consistently. Bump the station build to v1.0.6.
This commit is contained in:
@@ -811,9 +811,9 @@
|
|||||||
|
|
||||||
<!-- 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-general" multiple accept=".wav"
|
<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"
|
<input type="file" id="upload-station" multiple accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm"
|
||||||
onchange="performUpload('stationAudioFiles', '/api/upload/station-audio', this)">
|
onchange="performUpload('stationAudioFiles', '/api/upload/station-audio', this)">
|
||||||
|
|
||||||
<!-- Preview Modal -->
|
<!-- Preview Modal -->
|
||||||
@@ -1322,7 +1322,7 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
content.innerHTML = `<div style="color:var(--danger)">Failed to load content</div>`;
|
content.innerHTML = `<div style="color:var(--danger)">Failed to load content</div>`;
|
||||||
}
|
}
|
||||||
} else if (file.name.endsWith('.wav')) {
|
} else if (/\.(wav|mp3|ogg|flac|m4a|aac|opus)$/i.test(file.name)) {
|
||||||
saveBtn.style.display = 'none';
|
saveBtn.style.display = 'none';
|
||||||
content.innerHTML = `
|
content.innerHTML = `
|
||||||
<div class="audio-player">
|
<div class="audio-player">
|
||||||
|
|||||||
@@ -185,7 +185,7 @@
|
|||||||
<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>
|
||||||
<button class="btn btn-primary" onclick="document.getElementById('local_file_input').click()">从本地选择</button>
|
<button class="btn btn-primary" onclick="document.getElementById('local_file_input').click()">从本地选择</button>
|
||||||
<input type="file" id="local_file_input" accept=".wav,.dfpwm" style="display:none" onchange="uploadLocalFile(this)" />
|
<input type="file" id="local_file_input" accept=".wav,.mp3,.ogg,.flac,.m4a,.aac,.opus,.dfpwm" style="display:none" onchange="uploadLocalFile(this)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -363,7 +363,7 @@
|
|||||||
await loadTracks();
|
await loadTracks();
|
||||||
|
|
||||||
// Try to select the uploaded file
|
// Try to select the uploaded file
|
||||||
const baseName = String(file.name || "").replace(/\.(wav|dfpwm)$/i, "").trim();
|
const baseName = String(file.name || "").replace(/\.(wav|mp3|ogg|flac|m4a|aac|opus|dfpwm)$/i, "").trim();
|
||||||
const sel = document.getElementById("track_select");
|
const sel = document.getElementById("track_select");
|
||||||
for (let i = 0; i < sel.options.length; i++) {
|
for (let i = 0; i < sel.options.length; i++) {
|
||||||
if (sel.options[i].value === baseName) {
|
if (sel.options[i].value === baseName) {
|
||||||
|
|||||||
+34
-14
@@ -43,6 +43,8 @@ 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');
|
||||||
const tempBroadcastDir = path.join(appRootDir, 'temp-broadcast');
|
const tempBroadcastDir = path.join(appRootDir, 'temp-broadcast');
|
||||||
|
const STREAMABLE_AUDIO_EXTENSIONS = ['.wav', '.mp3', '.ogg', '.flac', '.m4a', '.aac', '.opus'];
|
||||||
|
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 });
|
||||||
@@ -446,10 +448,25 @@ async function sendLiveWavChunksToStation(stationName, fallbackWs, task) {
|
|||||||
|
|
||||||
const stationMusicStates = new Map();
|
const stationMusicStates = new Map();
|
||||||
|
|
||||||
|
function hasSupportedAudioExtension(name, exts = MUSIC_AUDIO_EXTENSIONS) {
|
||||||
|
const lower = String(name || '').trim().toLowerCase();
|
||||||
|
return exts.some(ext => lower.endsWith(ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripSupportedAudioExtension(name, exts = MUSIC_AUDIO_EXTENSIONS) {
|
||||||
|
const value = String(name || '').trim();
|
||||||
|
for (const ext of exts) {
|
||||||
|
if (value.toLowerCase().endsWith(ext)) {
|
||||||
|
return value.slice(0, -ext.length).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeBaseName(name) {
|
function normalizeBaseName(name) {
|
||||||
const s = String(name || '').trim();
|
const s = stripSupportedAudioExtension(name);
|
||||||
if (!s) return null;
|
if (!s) return null;
|
||||||
const base = s.replace(/\.(wav|dfpwm)$/i, '').trim();
|
const base = s.trim();
|
||||||
return base || null;
|
return base || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +487,7 @@ function listMusicTracks() {
|
|||||||
if (!file || file.startsWith('.')) continue;
|
if (!file || file.startsWith('.')) continue;
|
||||||
const full = path.join(audioGeneralDir, file);
|
const full = path.join(audioGeneralDir, file);
|
||||||
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) continue;
|
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) continue;
|
||||||
if (!file.toLowerCase().endsWith('.wav') && !file.toLowerCase().endsWith('.dfpwm')) continue;
|
if (!hasSupportedAudioExtension(file)) continue;
|
||||||
const base = normalizeBaseName(file);
|
const base = normalizeBaseName(file);
|
||||||
if (base) out.add(base);
|
if (base) out.add(base);
|
||||||
}
|
}
|
||||||
@@ -514,28 +531,32 @@ function getPlaybackAudioByBaseName(baseName) {
|
|||||||
const base = normalizeBaseName(baseName);
|
const base = normalizeBaseName(baseName);
|
||||||
if (!base) throw new Error('Invalid track name');
|
if (!base) throw new Error('Invalid track name');
|
||||||
|
|
||||||
const wavCandidates = [
|
for (const ext of STREAMABLE_AUDIO_EXTENSIONS) {
|
||||||
path.join(audioGeneralDir, `${base}.wav`),
|
const candidates = [
|
||||||
path.join(audioStationDir, `${base}.wav`)
|
path.join(audioGeneralDir, `${base}${ext}`),
|
||||||
|
path.join(audioStationDir, `${base}${ext}`)
|
||||||
];
|
];
|
||||||
for (const p of wavCandidates) {
|
for (const p of candidates) {
|
||||||
if (fs.existsSync(p)) {
|
if (!fs.existsSync(p)) continue;
|
||||||
|
let durationSec = 0;
|
||||||
|
if (ext === '.wav') {
|
||||||
const buffer = fs.readFileSync(p);
|
const buffer = fs.readFileSync(p);
|
||||||
|
durationSec = getWavDurationSec(buffer) || 0;
|
||||||
|
}
|
||||||
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' : 'audio/general', path.basename(p)),
|
||||||
durationSec: getWavDurationSec(buffer) || 0
|
durationSec
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dfCandidates = [
|
for (const p of [
|
||||||
path.join(audioGeneralDir, `${base}.dfpwm`),
|
path.join(audioGeneralDir, `${base}.dfpwm`),
|
||||||
path.join(audioStationDir, `${base}.dfpwm`)
|
path.join(audioStationDir, `${base}.dfpwm`)
|
||||||
];
|
]) {
|
||||||
for (const p of dfCandidates) {
|
if (!fs.existsSync(p)) continue;
|
||||||
if (fs.existsSync(p)) {
|
|
||||||
const buffer = fs.readFileSync(p);
|
const buffer = fs.readFileSync(p);
|
||||||
return {
|
return {
|
||||||
buffer,
|
buffer,
|
||||||
@@ -543,7 +564,6 @@ function getPlaybackAudioByBaseName(baseName) {
|
|||||||
durationSec: buffer.length / 6000
|
durationSec: buffer.length / 6000
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Audio not found: ${base}`);
|
throw new Error(`Audio not found: ${base}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -2,7 +2,7 @@
|
|||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- 1. Configuration & Constants
|
-- 1. Configuration & Constants
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
local APP_VERSION_TEXT = "v1.0.5"
|
local APP_VERSION_TEXT = "v1.0.6"
|
||||||
|
|
||||||
local CONFIG = {
|
local CONFIG = {
|
||||||
CLOUD_URL = "ws://stabadmin.fse-media.group",
|
CLOUD_URL = "ws://stabadmin.fse-media.group",
|
||||||
@@ -491,7 +491,15 @@ function Audio.loadStationList()
|
|||||||
if codes and files then
|
if codes and files then
|
||||||
local list = {}
|
local list = {}
|
||||||
for f in files:gmatch("[^,]+") do
|
for f in files:gmatch("[^,]+") do
|
||||||
local cleanName = f:match("^%s*(.-)%s*$"):gsub("%.wav$", ""):gsub("%.dfpwm$", "")
|
local cleanName = f:match("^%s*(.-)%s*$")
|
||||||
|
:gsub("%.[Ww][Aa][Vv]$", "")
|
||||||
|
:gsub("%.[Mm][Pp]3$", "")
|
||||||
|
:gsub("%.[Oo][Gg][Gg]$", "")
|
||||||
|
:gsub("%.[Ff][Ll][Aa][Cc]$", "")
|
||||||
|
:gsub("%.[Mm]4[Aa]$", "")
|
||||||
|
:gsub("%.[Aa][Aa][Cc]$", "")
|
||||||
|
:gsub("%.[Oo][Pp][Uu][Ss]$", "")
|
||||||
|
:gsub("%.[Dd][Ff][Pp][Ww][Mm]$", "")
|
||||||
if cleanName ~= "" then
|
if cleanName ~= "" then
|
||||||
table.insert(list, cleanName)
|
table.insert(list, cleanName)
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user