This commit is contained in:
2026-05-01 22:08:50 -07:00
parent 8b7a1f81ad
commit e51aeae54e
9 changed files with 196 additions and 15 deletions

View File

@@ -1,5 +1,6 @@
import { spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import fs from 'node:fs/promises';
import { createServer } from 'node:http';
import path from 'node:path';
import { Readable } from 'node:stream';
@@ -17,6 +18,8 @@ const wss = new WebSocketServer({ noServer: true });
const PORT = Number(process.env.PORT ?? 3000);
const FFMPEG_PATH = process.env.FFMPEG_PATH ?? 'ffmpeg';
const RECENT_URLS_PATH = process.env.RECENT_URLS_PATH ?? path.join(__dirname, '..', 'data', 'recent-urls.json');
const RECENT_URL_LIMIT = clampInteger(process.env.RECENT_URL_LIMIT, 12, 1, 50);
const SESSION_TTL_MS = 60 * 60 * 1000;
const MAX_WS_BUFFER_BYTES = 12 * 1024 * 1024;
const JPEG_SOI = Buffer.from([0xff, 0xd8]);
@@ -31,6 +34,8 @@ const defaults = {
const sessions = new Map();
const sourceTokens = new Map();
let recentUrls = [];
let recentWrite = Promise.resolve();
app.disable('x-powered-by');
app.use(express.json({ limit: '32kb' }));
@@ -40,7 +45,17 @@ app.get('/api/health', (_request, response) => {
response.json({ ok: true, ffmpeg: FFMPEG_PATH });
});
app.post('/api/session', (request, response) => {
app.get('/api/recent-urls', (_request, response) => {
response.json({
urls: recentUrls.map((item) => ({
url: item.url,
displayUrl: redactSecrets(item.url),
lastPlayedAt: item.lastPlayedAt,
})),
});
});
app.post('/api/session', async (request, response) => {
let url;
try {
@@ -61,6 +76,12 @@ app.post('/api/session', (request, response) => {
lastUsedAt: Date.now(),
});
try {
await addRecentUrl(url);
} catch (error) {
console.warn(`Failed to store recent URL: ${error.message}`);
}
response.status(201).json({ id, options });
});
@@ -313,6 +334,8 @@ setInterval(() => {
}
}, 60 * 1000).unref();
await loadRecentUrls();
server.listen(PORT, () => {
console.log(`Frame stream app listening at http://localhost:${PORT}`);
});
@@ -362,6 +385,45 @@ function parseAudioBitrate(value) {
return /^\d{2,3}k$/i.test(value) ? value.toLowerCase() : defaults.audioBitrate;
}
async function loadRecentUrls() {
try {
const raw = await fs.readFile(RECENT_URLS_PATH, 'utf8');
const parsed = JSON.parse(raw);
const items = Array.isArray(parsed?.urls) ? parsed.urls : [];
recentUrls = items
.filter((item) => typeof item?.url === 'string' && typeof item?.lastPlayedAt === 'string')
.slice(0, RECENT_URL_LIMIT);
} catch (error) {
if (error.code !== 'ENOENT') {
console.warn(`Failed to read recent URLs: ${error.message}`);
}
recentUrls = [];
}
}
async function addRecentUrl(url) {
const lastPlayedAt = new Date().toISOString();
recentUrls = [
{ url, lastPlayedAt },
...recentUrls.filter((item) => item.url !== url),
].slice(0, RECENT_URL_LIMIT);
recentWrite = recentWrite.catch(() => {}).then(() => saveRecentUrls());
await recentWrite;
}
async function saveRecentUrls() {
const directory = path.dirname(RECENT_URLS_PATH);
const temporaryPath = `${RECENT_URLS_PATH}.${process.pid}.tmp`;
const payload = `${JSON.stringify({ urls: recentUrls }, null, 2)}\n`;
await fs.mkdir(directory, { recursive: true });
await fs.writeFile(temporaryPath, payload, 'utf8');
await fs.rename(temporaryPath, RECENT_URLS_PATH);
}
function getSession(sessionId) {
const session = sessions.get(sessionId);