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");