Jeopardy / web /server.js
Aman-2804's picture
Upload folder using huggingface_hub
7822956 verified
Raw
History Blame Contribute Delete
9.73 kB
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
// Enable CORS for React frontend
app.use(cors())
app.use(express.json())
// Path to the database
const dbPath = path.join(__dirname, '..', 'jarchive.sqlite3')
const scrapeScriptPath = path.join(__dirname, '..', 'scrape_jarchive.py')
// Helper function to scrape a new game
async function scrapeNewGame() {
try {
console.log('Scraping new game...')
// Run from the parent directory where schema.sql and the script are located
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
}
}
// Helper function to delete all games from database
function deleteAllGames(db) {
return new Promise((resolve, reject) => {
// Check if tables exist first
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() // If database doesn't exist, that's fine
return
}
if (!row) {
console.log('Shows table does not exist yet, skipping delete')
resolve()
return
}
db.run('DELETE FROM shows', function(err) {
if (err) {
// If table doesn't exist or other error, just log and continue
console.log('Could not delete games (table might not exist):', err.message)
resolve() // Don't reject, just continue
} else {
console.log(`Deleted all games from database`)
resolve()
}
})
})
})
}
// Get a random complete game (always scrapes a new game on app load)
app.get('/api/random-game', async (req, res) => {
let db = null
try {
// Try to open database (will create if doesn't exist)
db = new sqlite3.Database(dbPath)
// Always delete all existing games first (if database exists)
await deleteAllGames(db)
db.close()
db = null
// Scrape a new random game
console.log('Scraping new random game...')
await scrapeNewGame()
// Reopen database after scraping and get the new game
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
})
}
})
// Helper function to get and return a random game
function getRandomGame(db, res) {
// First, try to get the most recently added game (the one we just scraped)
// This ensures we return the game we just added even if it's not "complete"
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 no game found, try a less strict query for any game with jeopardy round
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)
})
}
// Helper function to process and return game data
function processGame(db, res, showId, showNumber) {
const approxYear = showNumber ? (1984 + Math.floor((showNumber - 1) / 230)) : null
// Get categories and clues for jeopardy round
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' })
})
})
}
// Get double jeopardy round
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' })
})
})
})
// Get final jeopardy
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
})
})
})
// Delete a game from the database
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` })
})
})
// Serve static files from the React app (after API routes)
app.use(express.static(path.join(__dirname, "dist")))
// Catch-all handler: send back React's index.html file for any non-API routes
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}`)
})