// Seed du contenu quiz. One-shot au déploiement : bun run db:seed // - Open Trivia DB : catégories geek (mcq + truefalse), respect 1 req / 5 s. // - Banque FR en dur : insérée comme source 'manual'. // Idempotent : prompt unique → onConflictDoNothing. import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/postgres-js" import postgres from "postgres" import { env } from "../env" import { quizCategory, quizQuestion, type NewQuizQuestion, } from "./schema" import { QUIZ_QUESTIONS } from "../game/modes/quiz/questions" if (!env.databaseUrl) { console.error("DATABASE_URL manquant — impossible de seeder.") process.exit(1) } const client = postgres(env.databaseUrl, { max: 1 }) const db = drizzle(client, { schema: { quizCategory, quizQuestion } }) // Catégories Open Trivia DB pertinentes (id OpenTDB → nom affiché). const OPENTDB_CATEGORIES = [ { id: 15, name: "Jeux vidéo" }, { id: 31, name: "Anime & Manga" }, { id: 29, name: "Comics" }, { id: 11, name: "Cinéma" }, { id: 14, name: "Télévision" }, { id: 18, name: "Informatique" }, ] const AMOUNT = 30 const RATE_LIMIT_MS = 5200 // OpenTDB : 1 requête / 5 s par IP const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) const decode = (s: string) => Buffer.from(s, "base64").toString("utf8") const difficultyOf = (d: string) => (d === "hard" ? 3 : d === "medium" ? 2 : 1) // Tous les champs string sont encodés en base64 (encode=base64). interface OtdbQuestion { type: string difficulty: string question: string correct_answer: string incorrect_answers: string[] } function shuffle(items: T[]): T[] { const arr = [...items] for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[arr[i], arr[j]] = [arr[j], arr[i]] } return arr } async function getToken(): Promise { try { const res = await fetch("https://opentdb.com/api_token.php?command=request") const json = (await res.json()) as { token?: string } return json.token } catch { return undefined } } async function fetchBatch( categoryId: number, type: "multiple" | "boolean", token: string | undefined ): Promise { const url = `https://opentdb.com/api.php?amount=${AMOUNT}&category=${categoryId}` + `&type=${type}&encode=base64${token ? `&token=${token}` : ""}` const res = await fetch(url) const json = (await res.json()) as { response_code: number results: OtdbQuestion[] } if (json.response_code === 5) { // Rate limit : on attend et on réessaie une fois. await sleep(RATE_LIMIT_MS) return fetchBatch(categoryId, type, token) } return json.response_code === 0 ? json.results : [] } /** Upsert une catégorie et renvoie son id. */ async function upsertCategory(name: string): Promise { await db.insert(quizCategory).values({ name }).onConflictDoNothing() const [row] = await db .select({ id: quizCategory.id }) .from(quizCategory) .where(eq(quizCategory.name, name)) return row.id } function toQuestion( q: OtdbQuestion, categoryId: string ): NewQuizQuestion { // Avec encode=base64, TOUS les champs sont encodés (type/difficulty inclus). const prompt = decode(q.question) const type = decode(q.type) const difficulty = difficultyOf(decode(q.difficulty)) if (type === "boolean") { const correct = decode(q.correct_answer) // "True" | "False" return { categoryId, format: "truefalse", prompt, difficulty, source: "opentdb", lang: "en", choices: ["True", "False"], correctIndex: correct === "True" ? 0 : 1, } } const correct = decode(q.correct_answer) const choices = shuffle([ correct, ...q.incorrect_answers.map(decode), ]) return { categoryId, format: "mcq", prompt, difficulty, source: "opentdb", lang: "en", choices, correctIndex: choices.indexOf(correct), } } async function insertQuestions(rows: NewQuizQuestion[]): Promise { if (rows.length === 0) { return 0 } const inserted = await db .insert(quizQuestion) .values(rows) .onConflictDoNothing() .returning({ id: quizQuestion.id }) return inserted.length } async function seedOpenTdb(): Promise { const token = await getToken() let total = 0 let first = true for (const cat of OPENTDB_CATEGORIES) { const categoryId = await upsertCategory(cat.name) for (const type of ["multiple", "boolean"] as const) { if (!first) { await sleep(RATE_LIMIT_MS) } first = false const batch = await fetchBatch(cat.id, type, token) const rows = batch.map((q) => toQuestion(q, categoryId)) const n = await insertQuestions(rows) total += n console.log(` ${cat.name} (${type}): +${n}`) } } return total } async function seedManual(): Promise { let total = 0 for (const q of QUIZ_QUESTIONS) { const categoryId = await upsertCategory(q.category) total += await insertQuestions([ { categoryId, format: q.format, prompt: q.prompt, difficulty: q.difficulty, source: "manual", lang: "fr", choices: q.choices, correctIndex: q.correctIndex, acceptedAnswers: q.acceptedAnswers, }, ]) } return total } console.log("Seed Open Trivia DB (respect du 1 req / 5 s, patientez)…") const otdb = await seedOpenTdb() console.log("Seed banque FR (manual)…") const manual = await seedManual() console.log(`Terminé : ${otdb} questions OpenTDB + ${manual} manuelles.`) await client.end()