Module split: extract audio I/O helpers to server/audio.js

• getAudioDuration(path)         — ffprobe wrapper, returns seconds | null
  • splitAudioFile(in, dir, secs)  — ffmpeg -acodec copy chunking
  • downloadPodcastAudio(url, dst) — streams HTTP audio to disk

Also moved fetchUrl into util.js (alongside the other stateless
helpers) — it's a generic HTTP-GET-with-redirects used by RSS parsing
and channel discovery, not strictly audio.

server/index.js: 2758 → 2694 lines.

Smoke tested: server boots; /api/license-status, /api/health, /
respond. No behavior change.
This commit is contained in:
Keysat
2026-05-08 16:53:06 -05:00
parent 1c78e46ebd
commit 4c3cb6a077
3 changed files with 122 additions and 76 deletions
+9 -73
View File
@@ -17,8 +17,14 @@ import {
parseTimestampedTranscript,
safeText,
retryGemini,
fetchUrl,
} from "./util.js";
import { calcCost, buildAnalysisPrompt } from "./gemini-helpers.js";
import {
getAudioDuration,
splitAudioFile,
downloadPodcastAudio,
} from "./audio.js";
const execFileAsync = promisify(execFile);
const app = express();
@@ -949,19 +955,7 @@ async function fetchUploadDates(videoIds) {
// ── RSS-based date fetching (bypasses bot detection) ─────────────────────
// Fetch a URL and return the response body as a string
function fetchUrl(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return fetchUrl(res.headers.location).then(resolve, reject);
}
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => resolve(data));
res.on("error", reject);
}).on("error", reject);
});
}
// fetchUrl moved to ./util.js
// Get channel_id from a YouTube channel/playlist URL using yt-dlp
async function getChannelId(url) {
@@ -1086,26 +1080,7 @@ async function parsePodcastRSS(feedUrl, limit = 200) {
}
// Download a podcast episode audio file via HTTP(S) to a local path
function downloadPodcastAudio(audioUrl, destPath) {
return new Promise((resolve, reject) => {
const doFetch = (url) => {
const getter = url.startsWith("https") ? https : http;
getter.get(url, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return doFetch(res.headers.location);
}
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode} downloading podcast audio`));
}
const fileStream = createWriteStream(destPath);
res.pipe(fileStream);
fileStream.on("finish", () => fileStream.close(resolve));
fileStream.on("error", reject);
}).on("error", reject);
};
doFetch(audioUrl);
});
}
// downloadPodcastAudio moved to ./audio.js
// Get channel name from URL
async function fetchChannelName(url) {
@@ -2638,46 +2613,7 @@ Return ONLY the timestamped transcript, nothing else.`;
// ── Helpers ────────────────────────────────────────────────────────────────
// ── Audio duration helper (ffprobe) ─────────────────────────────────────
async function getAudioDuration(filePath) {
try {
const { stdout } = await execFileAsync("ffprobe", [
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
filePath,
], { timeout: 15000 });
const dur = parseFloat(stdout.trim());
return isNaN(dur) ? null : dur;
} catch {
return null;
}
}
// ── Split audio into chunks with ffmpeg ─────────────────────────────────
async function splitAudioFile(inputPath, outputDir, chunkSeconds = 2700) {
const duration = await getAudioDuration(inputPath);
if (!duration || duration <= chunkSeconds) return null; // no split needed
const chunks = [];
let startSec = 0;
let i = 0;
while (startSec < duration) {
const chunkPath = path.join(outputDir, `chunk_${i}.mp3`);
const segLen = Math.min(chunkSeconds, duration - startSec);
await execFileAsync("ffmpeg", [
"-y", "-i", inputPath,
"-ss", String(startSec),
"-t", String(segLen),
"-acodec", "copy",
chunkPath,
], { timeout: 120000 });
chunks.push({ path: chunkPath, startOffset: startSec, index: i });
startSec += chunkSeconds;
i++;
}
return chunks;
}
// getAudioDuration + splitAudioFile moved to ./audio.js
// sendEvent / extractVideoId / formatTime / parseTimestampedTranscript moved to ./util.js