aboutsummaryrefslogblamecommitdiffstats
path: root/src/scorecorder.zig
blob: 3d72dc121a26270da89a386229cf0b8dc55a09b2 (plain) (tree)














































































































































































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