| import express from 'express' |
| import sqlite3 from 'sqlite3' |
| import { fileURLToPath } from 'url' |
| import path from 'path' |
| import { exec } from 'child_process' |
| import { promisify } from 'util' |
| import cors from 'cors' |
|
|
| const execAsync = promisify(exec) |
|
|
| const __filename = fileURLToPath(import.meta.url) |
| const __dirname = path.dirname(__filename) |
|
|
| const app = express() |
| const PORT = process.env.PORT || 3001 |
|
|
| |
| app.use(cors()) |
| app.use(express.json()) |
|
|
| |
| const dbPath = path.join(__dirname, '..', 'jarchive.sqlite3') |
| const scrapeScriptPath = path.join(__dirname, '..', 'scrape_jarchive.py') |
|
|
| |
| async function scrapeNewGame() { |
| try { |
| console.log('Scraping new game...') |
| |
| const scriptDir = path.join(__dirname, '..') |
| const { stdout, stderr } = await execAsync(`python3 scrape_jarchive.py`, { |
| cwd: scriptDir |
| }) |
| if (stderr && !stderr.includes('NotOpenSSLWarning')) { |
| console.error('Scrape script stderr:', stderr) |
| } |
| console.log('Scrape script output:', stdout) |
| return true |
| } catch (error) { |
| console.error('Error scraping game:', error) |
| console.error('Error details:', error.message) |
| throw error |
| } |
| } |
|
|
| |
| function deleteAllGames(db) { |
| return new Promise((resolve, reject) => { |
| |
| db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='shows'", (err, row) => { |
| if (err) { |
| console.log('Database might not exist yet, skipping delete') |
| resolve() |
| return |
| } |
| if (!row) { |
| console.log('Shows table does not exist yet, skipping delete') |
| resolve() |
| return |
| } |
| db.run('DELETE FROM shows', function(err) { |
| if (err) { |
| |
| console.log('Could not delete games (table might not exist):', err.message) |
| resolve() |
| } else { |
| console.log(`Deleted all games from database`) |
| resolve() |
| } |
| }) |
| }) |
| }) |
| } |
|
|
| |
| app.get('/api/random-game', async (req, res) => { |
| let db = null |
| try { |
| |
| db = new sqlite3.Database(dbPath) |
| |
| |
| await deleteAllGames(db) |
| db.close() |
| db = null |
| |
| |
| console.log('Scraping new random game...') |
| await scrapeNewGame() |
| |
| |
| db = new sqlite3.Database(dbPath) |
| return getRandomGame(db, res) |
| } catch (error) { |
| console.error('Error in random-game endpoint:', error) |
| console.error('Error stack:', error.stack) |
| if (db) db.close() |
| return res.status(500).json({ |
| error: 'Failed to load or scrape game', |
| details: process.env.NODE_ENV === 'development' ? error.message : undefined |
| }) |
| } |
| }) |
|
|
| |
| function getRandomGame(db, res) { |
| |
| |
| db.get(` |
| SELECT s.id, s.show_number |
| FROM shows s |
| WHERE EXISTS ( |
| SELECT 1 FROM rounds r WHERE r.show_id = s.id AND r.name = 'jeopardy' |
| ) |
| ORDER BY s.id DESC |
| LIMIT 1 |
| `, (err, game) => { |
| if (err) { |
| console.error('Database error:', err) |
| db.close() |
| return res.status(500).json({ error: 'Database error' }) |
| } |
| |
| |
| if (!game) { |
| db.get(` |
| SELECT s.id, s.show_number |
| FROM shows s |
| WHERE EXISTS ( |
| SELECT 1 FROM rounds r WHERE r.show_id = s.id AND r.name = 'jeopardy' |
| ) |
| ORDER BY RANDOM() |
| LIMIT 1 |
| `, (err, game2) => { |
| if (err || !game2) { |
| console.error('No games found in database') |
| db.close() |
| return res.status(404).json({ error: 'No games found in database' }) |
| } |
| return processGame(db, res, game2.id, game2.show_number) |
| }) |
| return |
| } |
| |
| return processGame(db, res, game.id, game.show_number) |
| }) |
| } |
|
|
| |
| function processGame(db, res, showId, showNumber) { |
| const approxYear = showNumber ? (1984 + Math.floor((showNumber - 1) / 230)) : null |
| |
| |
| db.all(` |
| SELECT c.position, c.name, c.id |
| FROM categories c |
| JOIN rounds r ON c.round_id = r.id |
| WHERE r.show_id = ? AND r.name = 'jeopardy' |
| ORDER BY c.position |
| LIMIT 6 |
| `, [showId], (err, categories) => { |
| if (err) { |
| console.error('Categories error:', err) |
| db.close() |
| return res.status(500).json({ error: 'Failed to load categories' }) |
| } |
| |
| const categoryPromises = categories.map(cat => { |
| return new Promise((resolve, reject) => { |
| db.all(` |
| SELECT id, question, answer, value, row_index, is_daily_double |
| FROM clues |
| WHERE category_id = ? |
| ORDER BY row_index |
| `, [cat.id], (err, clues) => { |
| if (err) reject(err) |
| else resolve({ |
| title: cat.name, |
| clues: clues.map(c => ({ |
| id: c.id, |
| question: c.question, |
| answer: c.answer, |
| value: c.value, |
| rowIndex: c.row_index, |
| isDailyDouble: c.is_daily_double === 1 |
| })) |
| }) |
| }) |
| }) |
| }) |
| |
| Promise.all(categoryPromises) |
| .then(categoriesData => { |
| db.close() |
| res.json({ |
| showId, |
| showNumber, |
| approxYear, |
| categories: categoriesData |
| }) |
| }) |
| .catch(err => { |
| console.error('Clues error:', err) |
| db.close() |
| res.status(500).json({ error: 'Failed to load clues' }) |
| }) |
| }) |
| } |
|
|
| |
| app.get('/api/game/:showId/double', (req, res) => { |
| const db = new sqlite3.Database(dbPath) |
| const showId = req.params.showId |
| |
| db.all(` |
| SELECT c.position, c.name, c.id |
| FROM categories c |
| JOIN rounds r ON c.round_id = r.id |
| WHERE r.show_id = ? AND r.name = 'double' |
| ORDER BY c.position |
| LIMIT 6 |
| `, [showId], (err, categories) => { |
| if (err) { |
| console.error('Categories error:', err) |
| db.close() |
| return res.status(500).json({ error: 'Failed to load categories' }) |
| } |
| |
| const categoryPromises = categories.map(cat => { |
| return new Promise((resolve, reject) => { |
| db.all(` |
| SELECT id, question, answer, value, row_index, is_daily_double |
| FROM clues |
| WHERE category_id = ? |
| ORDER BY row_index |
| `, [cat.id], (err, clues) => { |
| if (err) reject(err) |
| else resolve({ |
| title: cat.name, |
| clues: clues.map(c => ({ |
| id: c.id, |
| question: c.question, |
| answer: c.answer, |
| value: c.value, |
| rowIndex: c.row_index, |
| isDailyDouble: c.is_daily_double === 1 |
| })) |
| }) |
| }) |
| }) |
| }) |
| |
| Promise.all(categoryPromises) |
| .then(categoriesData => { |
| db.close() |
| res.json({ categories: categoriesData }) |
| }) |
| .catch(err => { |
| console.error('Clues error:', err) |
| db.close() |
| res.status(500).json({ error: 'Failed to load clues' }) |
| }) |
| }) |
| }) |
|
|
| |
| app.get('/api/game/:showId/final', (req, res) => { |
| const db = new sqlite3.Database(dbPath) |
| const showId = req.params.showId |
| |
| db.get(` |
| SELECT cl.id, cl.question, cl.answer, c.name as category |
| FROM rounds r |
| JOIN categories c ON c.round_id = r.id |
| JOIN clues cl ON cl.category_id = c.id |
| WHERE r.show_id = ? AND r.name = 'final' |
| LIMIT 1 |
| `, [showId], (err, clue) => { |
| db.close() |
| |
| if (err) { |
| console.error('Final jeopardy error:', err) |
| return res.status(500).json({ error: 'Failed to load final jeopardy' }) |
| } |
| |
| if (!clue) { |
| return res.status(404).json({ error: 'No final jeopardy found' }) |
| } |
| |
| res.json({ |
| id: clue.id, |
| question: clue.question, |
| answer: clue.answer, |
| category: clue.category |
| }) |
| }) |
| }) |
|
|
| |
| app.delete('/api/game/:showId', (req, res) => { |
| const db = new sqlite3.Database(dbPath) |
| const showId = req.params.showId |
| |
| db.run('DELETE FROM shows WHERE id = ?', [showId], function(err) { |
| if (err) { |
| console.error('Error deleting game:', err) |
| db.close() |
| return res.status(500).json({ error: 'Failed to delete game' }) |
| } |
| |
| db.close() |
| console.log(`Game ${showId} deleted from database`) |
| res.json({ success: true, message: `Game ${showId} deleted` }) |
| }) |
| }) |
|
|
| |
| app.use(express.static(path.join(__dirname, "dist"))) |
|
|
| |
| app.get("*", (req, res) => { |
| res.sendFile(path.join(__dirname, "dist", "index.html")) |
| }) |
|
|
| app.listen(PORT, '0.0.0.0', () => { |
| console.log(`🎮 Jeopardy API server running on http://0.0.0.0:${PORT}`) |
| }) |
|
|