Files
coursehub/tools/build-catalog-seed.php
T
Harwinder Sekhon 090d7fa94c Seed the vendor catalogue into SQL and add an optional AI course advisor
Course metadata: a scan now gives every course a course_meta row from the
downloader's sidecar JSON when the folder has one, else from catalog_courses
matched by the folder name the downloader would create (folderKeyFor, a port
of ine.py's sanitize_name). tools/build-catalog-seed.php turns the INE export
into seeds/ine_catalog.sql, an idempotent patch that creates its own table and
upserts by id, so it can be applied to the live server before or after the
code and re-run freely. Course pages show the blurb, difficulty, vendor and
tags; search matches tags and blurbs; Settings reports how each course got its
details. Against the live library 163 of 179 courses match.

Course advisor, off by default: api/advisor.php answers "which course should I
take to learn X / what comes before Y" with courses from the library, in
order, plus vendor courses not yet downloaded. includes/ai_client.php is a
small curl wrapper for any OpenAI-format chat endpoint (DeepSeek V4 Flash by
default, no composer); the prompt is a byte-stable catalogue of the library so
the provider's prefix cache covers it. Every course id the model returns is
validated server-side, a bad JSON reply gets one retry then a plain-text
fallback, and a per-user daily cap with token counts lives in
advisor_requests. With AI_ENABLED false the app makes no outbound requests
and the box does not appear; users can also hide it per browser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012enquL2RZdjy3D5AcKP5yx
2026-09-08 18:36:43 -04:00

88 lines
3.6 KiB
PHP

<?php
// Build an SQL seed of the vendor's course catalogue from its JSON export:
//
// php tools/build-catalog-seed.php ~/ine-dl/all_courses_with_access.json > seeds/ine_catalog.sql
//
// The output is self-contained and idempotent — it creates `catalog_courses`
// if needed and upserts every course — so it can be applied to any install,
// before or after a code deploy, as many times as you like:
//
// mysql coursehub < seeds/ine_catalog.sql && php scan.php
//
// The scan is what matches library folders to the seeded rows (course_meta).
// seeds/ is git-ignored: the file carries the vendor's course descriptions.
require_once __DIR__ . '/../includes/functions.php';
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
$src = $argv[1] ?? '';
if ($src === '' || !is_file($src)) {
fwrite(STDERR, "usage: php tools/build-catalog-seed.php <all_courses.json> > seeds/ine_catalog.sql\n");
exit(1);
}
ini_set('memory_limit', '512M');
$raw = file_get_contents($src);
$all = json_decode($raw, true);
if (!is_array($all)) {
fwrite(STDERR, "not a JSON array: $src\n");
exit(1);
}
// Some exports wrap the list.
if (isset($all['results']) && is_array($all['results'])) $all = $all['results'];
if (isset($all['courses']) && is_array($all['courses'])) $all = $all['courses'];
/** MySQL string literal (backslash escaping, the server's default mode). */
function sqlQuote(?string $v): string
{
if ($v === null) return 'NULL';
return "'" . str_replace(
["\\", "'", "\0", "\n", "\r", "\x1a"],
["\\\\", "\\'", "\\0", "\\n", "\\r", "\\Z"],
$v
) . "'";
}
$json = fn(array $a) => json_encode($a, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$rows = [];
$skipped = 0;
foreach ($all as $entry) {
if (!is_array($entry)) { $skipped++; continue; }
$f = catalogFields($entry);
if ($f['id'] === null || $f['name'] === null || $f['slug'] === null) { $skipped++; continue; }
$rows[$f['id']] = [
sqlQuote($f['id']), sqlQuote($f['name']), sqlQuote($f['slug']),
sqlQuote(folderKeyFor($f['name'])), sqlQuote($f['vendor']), sqlQuote($f['difficulty']),
sqlQuote($json($f['tags'])), sqlQuote($f['short_description']), sqlQuote($f['description']),
sqlQuote($json($f['learning_paths'])), sqlQuote($json($f['categories'])),
$f['duration_seconds'] === null ? 'NULL' : (string)$f['duration_seconds'],
sqlQuote($f['url']),
];
}
ksort($rows); // stable output: the same export always produces the same file
$cols = 'id, name, slug, folder_key, vendor, difficulty, tags, short_description, description,'
. ' learning_paths, categories, duration_seconds, url';
$upd = implode(', ', array_map(
fn($c) => "$c = VALUES($c)",
['name', 'slug', 'folder_key', 'vendor', 'difficulty', 'tags', 'short_description',
'description', 'learning_paths', 'categories', 'duration_seconds', 'url']
));
echo "-- CourseHub catalogue seed — generated by tools/build-catalog-seed.php\n";
echo '-- source: ' . basename($src) . ' (modified ' . gmdate('Y-m-d H:i:s', filemtime($src) ?: 0) . " UTC)\n";
echo '-- courses: ' . count($rows) . ($skipped ? " ($skipped entries skipped)" : '') . "\n";
echo "-- Safe to re-run: creates the table if needed and upserts by id.\n";
echo "SET NAMES utf8mb4;\n";
echo catalogCoursesDdl() . ";\n";
foreach (array_chunk(array_values($rows), 50) as $chunk) {
echo "INSERT INTO catalog_courses ($cols) VALUES\n";
echo implode(",\n", array_map(fn($r) => '(' . implode(', ', $r) . ')', $chunk));
echo "\nON DUPLICATE KEY UPDATE $upd;\n";
}
fwrite(STDERR, 'wrote ' . count($rows) . " courses\n");