const Round = struct {
events: []Event,
};
const Event = struct {
strEvent: []u8,
idHomeTeam: []u8,
strHomeTeam: []u8,
idAwayTeam: []u8,
strAwayTeam: []u8,
intHomeScore: ?[]u8 = null,
intAwayScore: ?[]u8 = null,
strStatus: []u8,
dateEvent: []u8,
strTimestamp: []u8,
};
const Config = struct {
round: i16,
season: i16,
all_rounds: bool = false,
};
var app: App = undefined;
var config: Config = undefined;
var client: zul.http.Client = undefined;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// initialize a logging pool
try logz.setup(allocator, .{
.level = .Info,
.pool_size = 100,
.buffer_size = 4096,
.large_buffer_count = 8,
.large_buffer_size = 16384,
.output = .stdout,
.encoding = .logfmt,
});
defer logz.deinit();
app = try App.init(allocator);
defer app.deinit();
var args = try zul.CommandLineArgs.parse(allocator);
defer args.deinit();
client = zul.http.Client.init(allocator);
defer client.deinit();
config = .{
// .round = try std.fmt.parseInt(i16, args.get("round"), 10) orelse currentWeek(),
.round = try weekToRound(if (args.contains("round")) try std.fmt.parseInt(i16, args.get("round").?, 10) else currentRound()),
.season = if (args.contains("season")) try std.fmt.parseInt(i16, args.get("season").?, 10) else currentSeason(),
.all_rounds = args.contains("all"),
};
logz.info()
.int("round", config.round)
.int("season", config.season)
.boolean("all_rounds", config.all_rounds)
.log();
if (config.all_rounds) {
for (1..23) |round| {
try recordRound(allocator, @intCast(round));
std.posix.nanosleep(1, 0);
}
} else {
try recordRound(allocator, config.round);
}
}
fn recordRound(allocator: std.mem.Allocator, round: i16) !void {
var req = try client.request("https://www.thesportsdb.com/api/v1/json/3/eventsround.php");
defer req.deinit();
// id is the league id, with 4391 == NFL
try req.query("id", "4391");
// the round - which corresponds to the week or playoff id
try req.query("r", try std.fmt.allocPrint(allocator, "{d}", .{round}));
// the season year
try req.query("s", try std.fmt.allocPrint(allocator, "{d}", .{config.season}));
const res = try req.getResponse(.{});
if (res.status != 200) {
// TODO: handle error
return;
}
var managed = try res.json(Round, allocator, .{ .ignore_unknown_fields = true });
defer managed.deinit();
const parsed_round = managed.value;
for (parsed_round.events) |event| {
logz.info()
.fmt("event", "{s} {s} @ {s} {s}", .{ event.strAwayTeam, event.intAwayScore orelse "0", event.strHomeTeam, event.intHomeScore orelse "0" })
.string("date", event.strTimestamp)
.string("status", event.strStatus)
.log();
if (!std.mem.eql(u8, "", event.strStatus) and !std.mem.eql(u8, "FT", event.strStatus) and !std.mem.eql(u8, "AOT", event.strStatus) and !std.mem.eql(u8, "Match Finished", event.strStatus)) {
logz.info().fmt("event", "{s} in progress or not started.", .{event.strEvent}).log();
continue;
}
const home_score = try std.fmt.parseInt(usize, event.intHomeScore orelse "0", 10);
const away_score = try std.fmt.parseInt(usize, event.intAwayScore orelse "0", 10);
const winner_id: []u8 = if (home_score > away_score) event.idHomeTeam else event.idAwayTeam;
logz.info().string("winner", winner_id).log();
try insertScore(winner_id, "win", event.strTimestamp);
if (config.round == 160 or config.round == 125) {
logz.info().string("playoffs", "Home Team made it to the playoffs").log();
try insertScore(event.idHomeTeam, "playoffs", event.strTimestamp);
logz.info().string("playoffs", "Away Team made it to the playoffs").log();
try insertScore(event.idAwayTeam, "playoffs", event.strTimestamp);
}
const category = switch (config.round) {
160 => "divisional",
125 => "conference",
150 => "superbowl",
200 => "champion",
else => null,
};
if (category) |c| {
logz.info().fmt("category", "Inserting {s} score for winner", .{c}).log();
try insertScore(winner_id, c, event.strTimestamp);
}
}
}
pub fn insertScore(external_id: []u8, category: []const u8, timestamp: []const u8) !void {
_ = app.pool.exec("SELECT record_external_score_by_year($1, $2, $3, $4, $5)", .{ external_id, config.season, config.round, @constCast(category), timestamp }) catch @panic("failed to record score");
}
fn currentRound() i16 {
var current_week = (app.pool.row("SELECT current_week()", .{}) catch @panic("can't fetch current week")) orelse unreachable;
defer current_week.deinit() catch {};
return current_week.get(i16, 0);
}
fn currentSeason() i16 {
const season_query =
\\SELECT date_part('year', started_at)::smallint
\\FROM seasons WHERE season_id = current_season();
;
var current_season = (app.pool.row(season_query, .{}) catch @panic("can't fetch current season")) orelse unreachable;
defer current_season.deinit() catch {};
return current_season.get(i16, 0);
}
fn weekToRound(week: i16) !i16 {
return switch (week) {
1...18 => |w| w,
19 => 160,
20 => 125,
21 => 150,
23 => 200,
else => return error.UnknownRound,
};
}
test "weekToRound" {
try std.testing.expect(try weekToRound(1) == 1);
try std.testing.expectError(error.UnknownRound, weekToRound(55));
}
const App = @import("app.zig").App;
const logz = @import("logz");
const std = @import("std");
const zul = @import("zul");