File size: 9,728 Bytes
7822956
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
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}`)
})