Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ yarn-error.log*
**/*.tar.gz
**/*.tgz
**/*.log
**/*.sqlite
package-lock.json
**/*.bun
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ It also requires different dates for different sports. Football uses YYYY, while

`GET /schedule/basketball-men/d1/2023/02`

### Team Schedule

Returns games for one school in one sport/division/season.

`GET /team-schedule/michigan/basketball-men/d1/2025`

### Brackets

Tournament bracket for a given sport, division, and year, including live scores.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun --watch src/index.ts",
"start": "NODE_ENV=production bun src/index.ts",
"lint": "biome check --write src"
"lint": "biome check --write src",
"team-schedule:ingest": "bun src/team-schedule/cli.ts"
},
"dependencies": {
"@elysiajs/openapi": "^1.4.11",
Expand Down
27 changes: 27 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import type { NewScoreboardParams } from "./scoreboard/types";
import * as v from 'valibot';
import { validDivisions, validGameIds, validScoreboardSports, validSports, validYears } from "./schema";
import { getTeamScheduleGames, openTeamScheduleDb } from "./team-schedule/db";

// 30 minute cache for most routes
const cache_30m = new ExpiryMap(30 * 60 * 1000);
Expand All @@ -43,6 +44,7 @@ const validRoutes = new Map([
["game", cache_45s],
["scoreboard", cache_45s],
["schedule-alt", cache_30m],
["team-schedule", cache_30m],
["news", cache_30m],
["brackets", cache_45s]
]);
Expand Down Expand Up @@ -445,6 +447,31 @@ export const app = new Elysia()
year: validYears,
})
})
.get("/team-schedule/:schoolSlug/:sport/:division/:season", async ({ cache, cacheKey, params }) => {
const db = openTeamScheduleDb();
try {
const season = Number(params.season);
const rows = getTeamScheduleGames(
db,
params.schoolSlug,
params.sport,
params.division,
season
);
const data = JSON.stringify(rows);
cache.set(cacheKey, data);
return data;
} finally {
db.close();
}
}, {
params: v.object({
schoolSlug: v.pipe(v.string(), v.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)),
sport: validSports,
division: validDivisions,
season: validYears,
})
})
// scoreboard route to fetch data from data.ncaa.com json endpoint
.get("/scoreboard/:sport/*", async ({ cache, cacheKey, params, set, status }) => {
const sportCodes = newCodesBySport[params.sport];
Expand Down
38 changes: 38 additions & 0 deletions src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,44 @@ export const openapiSpec = openapi({
},
],
},
"/team-schedule/{schoolSlug}/{sport}/{division}/{season}": {
get: {
responses: {},
summary: "Team schedule",
description:
"Team schedule for a school/sport/division/season. Returns one row per game with home and away teams.",
parameters: [
{
name: "schoolSlug",
in: "path",
schema: { type: "string" },
required: true,
examples: makeExamples(["michigan", "duke", "ucla"]),
},
{
name: "sport",
in: "path",
schema: { type: "string" },
required: true,
examples: makeExamples(["basketball-men", "basketball-women"]),
},
{
name: "division",
in: "path",
schema: { type: "string" },
required: true,
examples: makeExamples(["d1", "d2", "d3", "fbs", "fcs"]),
},
{
name: "season",
in: "path",
schema: { type: "string" },
required: true,
examples: makeExamples(["2025", "2024"]),
},
] as OpenAPIV3.ParameterObject[],
},
},
"/schools-index": {
get: {
responses: {},
Expand Down
80 changes: 80 additions & 0 deletions src/team-schedule/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { getSeasonYear } from "../codes";
import { getDefaultDbPath } from "./db";
import { ingestTeamSchedule } from "./ingest";

function getArg(flag: string) {
const prefix = `${flag}=`;
const pair = Bun.argv.find((arg) => arg.startsWith(prefix));
if (pair) {
return pair.slice(prefix.length);
}

const index = Bun.argv.findIndex((arg) => arg === flag);
if (index === -1) {
return undefined;
}
return Bun.argv[index + 1];
}

function hasFlag(flag: string) {
return Bun.argv.includes(flag);
}

function printUsage() {
console.log("Usage: bun src/team-schedule/cli.ts [options]");
console.log("");
console.log("Options:");
console.log(" --sport <value> default: basketball-men");
console.log(" --division <value> default: d1");
console.log(` --season-year <value> default: ${getSeasonYear(new Date())}`);
console.log(` --db-path <value> default: ${getDefaultDbPath()}`);
console.log(" --max-dates <value> process only first N dates");
console.log(" --delay-ms <value> delay between upstream fetches (default: 350)");
console.log(" --dry-run fetch and map only, do not write sqlite");
}

if (hasFlag("--help") || hasFlag("-h")) {
printUsage();
process.exit(0);
}

const sport = getArg("--sport") ?? "basketball-men";
const division = getArg("--division") ?? "d1";
const seasonYearRaw = getArg("--season-year");
const seasonYear = seasonYearRaw ? parseInt(seasonYearRaw, 10) : getSeasonYear(new Date());
const maxDatesRaw = getArg("--max-dates");
const maxDates = maxDatesRaw ? parseInt(maxDatesRaw, 10) : undefined;
const delayMsRaw = getArg("--delay-ms");
const delayMs = delayMsRaw ? parseInt(delayMsRaw, 10) : 350;
const dryRun = hasFlag("--dry-run");
const dbPath = getArg("--db-path") ?? getDefaultDbPath();

if (Number.isNaN(seasonYear)) {
throw new Error("Invalid --season-year value");
}
if (maxDatesRaw && Number.isNaN(maxDates)) {
throw new Error("Invalid --max-dates value");
}
if (delayMsRaw && Number.isNaN(delayMs)) {
throw new Error("Invalid --delay-ms value");
}

console.log(`Starting ingest for ${sport}/${division}/${seasonYear}`);
console.log(`Mode: ${dryRun ? "dry-run" : "write"}`);
console.log(`DB path: ${dbPath}`);
if (typeof maxDates === "number") {
console.log(`Date limit: ${maxDates}`);
}

const result = await ingestTeamSchedule({
sport,
division,
seasonYear,
dbPath,
maxDates,
delayMs,
dryRun,
});

console.log("Done");
console.log(JSON.stringify(result, null, 2));
Loading