Files
malabar/webnode/server.js
2026-03-03 17:42:50 +08:00

145 lines
3.6 KiB
JavaScript

/**
* Simple Express server to display translated episodes with speaker colors.
*/
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 5000;
// Configuration
const TRANSLATED_DIR = path.join(__dirname, '..', '_translated');
const COLORS_FILE = path.join(__dirname, '..', '_colors.json');
// Set up EJS as templating engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Serve static files
app.use(express.static(path.join(__dirname, 'public')));
/**
* Load speaker color mapping.
*/
function loadColors() {
try {
if (fs.existsSync(COLORS_FILE)) {
const data = fs.readFileSync(COLORS_FILE, 'utf-8');
return JSON.parse(data);
}
} catch (err) {
console.error('Error loading colors:', err);
}
return {};
}
/**
* Get list of all episodes from _translated folder.
*/
function getEpisodes() {
const episodes = [];
try {
if (fs.existsSync(TRANSLATED_DIR)) {
const files = fs.readdirSync(TRANSLATED_DIR);
files
.filter(f => f.endsWith('_translated.json'))
.sort()
.forEach(file => {
// Extract episode name from filename (e.g., "S02E01_translated.json" -> "S02E01")
const episodeId = file.replace('_translated.json', '');
episodes.push({
id: episodeId,
filename: file,
title: episodeId
});
});
}
} catch (err) {
console.error('Error reading episodes:', err);
}
return episodes;
}
/**
* Load a specific episode's data.
*/
function loadEpisode(episodeId) {
const jsonFile = path.join(TRANSLATED_DIR, `${episodeId}_translated.json`);
try {
if (fs.existsSync(jsonFile)) {
const data = fs.readFileSync(jsonFile, 'utf-8');
return JSON.parse(data);
}
} catch (err) {
console.error('Error loading episode:', err);
}
return null;
}
/**
* Escape HTML special characters.
*/
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// Make escapeHtml available to templates
app.locals.escapeHtml = escapeHtml;
// Routes
app.get('/', (req, res) => {
const episodes = getEpisodes();
if (episodes.length === 0) {
return res.status(404).send('No episodes found');
}
// Get first episode data
const firstEpisode = episodes[0];
const lines = loadEpisode(firstEpisode.id);
const colors = loadColors();
res.render('episode', {
episodes,
currentEpisode: firstEpisode,
lines,
colors
});
});
app.get('/episode/:episodeId', (req, res) => {
const episodeId = req.params.episodeId;
const lines = loadEpisode(episodeId);
if (!lines) {
return res.status(404).send('Episode not found');
}
const episodes = getEpisodes();
const currentEpisode = {
id: episodeId,
filename: `${episodeId}_translated.json`,
title: episodeId
};
const colors = loadColors();
res.render('episode', {
episodes,
currentEpisode,
lines,
colors
});
});
// Start server
app.listen(PORT, '0.0.0.0', () => {
console.log(`Episode Web Viewer running at http://localhost:${PORT}`);
});