175 lines
4.3 KiB
JavaScript
175 lines
4.3 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;
|
|
}
|
|
|
|
/**
|
|
* Parse timestamp string [mm:ss] to seconds.
|
|
*/
|
|
function parseTimestamp(timestamp) {
|
|
if (!timestamp) return 0;
|
|
const match = timestamp.match(/\[(\d+):(\d+)\]/);
|
|
if (match) {
|
|
const minutes = parseInt(match[1], 10);
|
|
const seconds = parseInt(match[2], 10);
|
|
return minutes * 60 + seconds;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Escape HTML special characters.
|
|
*/
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
return text
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
// 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];
|
|
let lines = loadEpisode(firstEpisode.id);
|
|
const colors = loadColors();
|
|
|
|
// Add timeSeconds to each line
|
|
if (lines && Array.isArray(lines)) {
|
|
lines = lines.map(line => ({
|
|
...line,
|
|
timeSeconds: parseTimestamp(line.timestamp)
|
|
}));
|
|
}
|
|
|
|
res.render('episode', {
|
|
episodes,
|
|
currentEpisode: firstEpisode,
|
|
lines,
|
|
colors
|
|
});
|
|
});
|
|
|
|
app.get('/episode/:episodeId', (req, res) => {
|
|
const episodeId = req.params.episodeId;
|
|
let 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();
|
|
|
|
// Add timeSeconds to each line
|
|
if (lines && Array.isArray(lines)) {
|
|
lines = lines.map(line => ({
|
|
...line,
|
|
timeSeconds: parseTimestamp(line.timestamp)
|
|
}));
|
|
}
|
|
|
|
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}`);
|
|
});
|