/** * Canlı YÖK Atlas API'sinden 2025 yerleştirme verisini çekip DB'yi tazeler. * * Endpoint: POST https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search * (yokatlas-py'nin kullandığı resmî tercih-kılavuz JSON API'si) * * - Mevcut programlarda sira/puan/kontenjan/yerlesen 2025 kolonlarını günceller * - DB'de olmayan (yeni açılan) programları ekler * - Az yerleşeni olan programlarda API basariSirasi döndürmez → NULL kalır * * Kullanım: npx tsx scripts/refresh.ts */ import path from "node:path"; import Database from "better-sqlite3"; const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search"; const PAGE_SIZE = 500; const DELAY_MS = 300; const TUR_MAP: Record = { SAY: "SAYISAL", EA: "EŞİT AĞIRLIK", SÖZ: "SÖZEL", DİL: "DİL", TYT: "TYT", }; type ApiRecord = { kilavuzKodu: number; birimAdi: string; universiteAdi: string; universiteTuru: string | null; ilAdi: string | null; fymkAdi: string | null; puanTuru: string; ogrenimSuresi: number | null; birimTuruId: number; basariSirasi?: number | null; minPuan?: number | null; kontenjan?: number | null; gkY?: number | null; sgyY?: number | null; dprmY?: number | null; obkY?: number | null; }; type ApiPage = { content: ApiRecord[]; totalPages: number; totalElements: number; }; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); async function fetchPage(page: number): Promise { const res = await fetch(API, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filters: { puanTuru: null, universiteId: [], birimGrupId: [], ilKodu: [], birimTuruId: null, universiteTuru: null, bursOraniId: null, ogrenimTuruId: null, kilavuzKodu: null, minBasariSirasi: null, maxBasariSirasi: null, }, page, size: PAGE_SIZE, sortBy: "kilavuzKodu", direction: "ASC", }), }); if (!res.ok) { throw new Error(`API ${res.status} (sayfa ${page})`); } return (await res.json()) as ApiPage; } function yerlesen(r: ApiRecord): number | null { const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter( (x): x is number => typeof x === "number" ); return parts.length ? parts.reduce((a, b) => a + b, 0) : null; } function intOrNull(v: unknown): number | null { const n = typeof v === "string" ? Number.parseFloat(v) : (v as number); return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.round(n) : null; } function realOrNull(v: unknown): number | null { const n = typeof v === "string" ? Number.parseFloat(v) : (v as number); return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : null; } async function main() { const db = new Database(path.join(process.cwd(), "data", "yokatlas.db")); db.pragma("journal_mode = WAL"); const cols = db.prepare("PRAGMA table_info(programs)").all() as { name: string; }[]; const has2025 = cols.some((c) => c.name === "sira2025"); if (!has2025) { db.exec(` ALTER TABLE programs ADD COLUMN sira2025 INTEGER; ALTER TABLE programs ADD COLUMN puan2025 REAL; ALTER TABLE programs ADD COLUMN kontenjan2025 INTEGER; ALTER TABLE programs ADD COLUMN yerlesen2025 INTEGER; CREATE INDEX IF NOT EXISTS idx_programs_tur_sira25 ON programs (tur, sira2025); `); } // Sorgu yolunun kullandığı ifade indexi; ifade src/lib/db.ts EFEKTIF_SIRA // ile birebir aynı olmalı (bkz. scripts/db-index.ts). sira2025 kolonu // yukarıda garanti edildikten sonra kurulur. db.exec(` CREATE INDEX IF NOT EXISTS idx_programs_tur_onlisans_efektif ON programs (tur, onlisans, COALESCE(sira2025, sira2024)); `); const update = db.prepare(` UPDATE programs SET sira2025 = ?, puan2025 = ?, kontenjan2025 = ?, yerlesen2025 = ? WHERE id = ? `); const insert = db.prepare(` INSERT INTO programs (id, isim, universite, unitur, il, fakulte, tur, sure, onlisans, sira2025, puan2025, kontenjan2025, yerlesen2025) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?"); let updated = 0; let inserted = 0; let skipped = 0; const first = await fetchPage(0); const totalPages = first.totalPages; console.log( `Toplam ${first.totalElements} kayıt, ${totalPages} sayfa çekilecek…` ); const processPage = db.transaction((records: ApiRecord[]) => { for (const r of records) { const tur = TUR_MAP[r.puanTuru?.trim() ?? ""]; if (!r.kilavuzKodu || !tur) { skipped++; continue; } const id = String(r.kilavuzKodu); const vals = [ intOrNull(r.basariSirasi), realOrNull(r.minPuan), intOrNull(r.kontenjan), yerlesen(r), ]; if (exists.get(id)) { update.run(...vals, id); updated++; } else { insert.run( id, r.birimAdi, r.universiteAdi, r.universiteTuru ?? null, r.ilAdi ?? null, r.fymkAdi ?? null, tur, r.ogrenimSuresi ?? null, r.birimTuruId === 47 ? 1 : 0, ...vals ); inserted++; } } }); processPage(first.content); for (let p = 1; p < totalPages; p++) { await sleep(DELAY_MS); const page = await fetchPage(p); processPage(page.content); process.stdout.write(`\rsayfa ${p + 1}/${totalPages}`); } console.log(); const stats = db .prepare( `SELECT COUNT(*) AS toplam, SUM(CASE WHEN sira2025 IS NOT NULL THEN 1 ELSE 0 END) AS sirali FROM programs` ) .get() as { toplam: number; sirali: number }; console.log( `Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${skipped}` ); console.log( `DB toplam: ${stats.toplam} program, 2025 sıralaması olan: ${stats.sirali}` ); db.close(); } main().catch((err) => { console.error(err); process.exit(1); });