package megamillions import ( "context" "fmt" "io" "strconv" "strings" "time" "gitea.stevedudenhoeffer.com/steve/go-extractor" "golang.org/x/text/currency" ) type Config struct{} var DefaultConfig = Config{} func (c Config) validate() Config { return c } type Drawing struct { Date time.Time Numbers [5]int MegaBall int Megaplier int } type NextDrawing struct { Date string Jackpot currency.Amount } func deferClose(cl io.Closer) { if cl != nil { _ = cl.Close() } } func netTicksToTime(t int64) time.Time { return time.Unix(0, t*100).Add(-621355968000000000) } func getDrawing(_ context.Context, doc extractor.Document) (*Drawing, error) { var drawing Drawing // the drawdate is stored as a .net ticks value in the data-playdateticks attribute of a // span with the id of "lastestDate" date := doc.Select("span#lastestDate") if len(date) != 1 { return nil, fmt.Errorf("expected 1 date, got %d", len(date)) } txt, err := date[0].Attr("data-playdateticks") if err != nil { return nil, fmt.Errorf("failed to get date: %w", err) } ticks, err := strconv.ParseInt(txt, 10, 64) if err != nil { return nil, fmt.Errorf("failed to parse date: %w", err) } fmt.Println("ticks", ticks) drawing.Date = netTicksToTime(ticks) err = doc.ForEach("ul.numbers li.ball", func(n extractor.Node) error { classes, err := n.Attr("class") if err != nil { return err } txt, err := n.Text() if err != nil { return err } val, err := strconv.Atoi(txt) if err != nil { return err } if strings.Contains(classes, "winNum1") { drawing.Numbers[0] = val return nil } if strings.Contains(classes, "winNum2") { drawing.Numbers[1] = val return nil } if strings.Contains(classes, "winNum3") { drawing.Numbers[2] = val return nil } if strings.Contains(classes, "winNum4") { drawing.Numbers[3] = val return nil } if strings.Contains(classes, "winNum5") { drawing.Numbers[4] = val return nil } if strings.Contains(classes, "winNumMB") { drawing.MegaBall = val return nil } return fmt.Errorf("unknown li.ball class: %s", classes) }) if err != nil { return nil, fmt.Errorf("failed to get numbers: %w", err) } megaplier := doc.Select("span.megaplier span.winNumMP") if len(megaplier) != 1 { return nil, fmt.Errorf("expected 1 megaplier, got %d", len(megaplier)) } // megaplier is in the format of "2X" or "3X" etc. txt, err = megaplier[0].Text() if err != nil { return nil, fmt.Errorf("failed to get megaplier: %w", err) } val, err := strconv.Atoi(strings.ReplaceAll(strings.ReplaceAll(txt, "X", ""), "x", "")) if err != nil { return nil, fmt.Errorf("failed to convert megaplier to int: %w", err) } drawing.Megaplier = val return &drawing, nil } func getNextDrawing(_ context.Context, doc extractor.Document) (*NextDrawing, error) { var nextDrawing NextDrawing date := doc.Select("div.nextEstGroup span.nextDrawDate") if len(date) != 1 { return nil, fmt.Errorf("expected 1 date, got %d", len(date)) } var err error nextDrawing.Date, err = date[0].Text() if err != nil { return nil, fmt.Errorf("failed to get date: %w", err) } jackpot := doc.Select("div.nextEstGroup span.nextEstVal") if len(jackpot) != 1 { return nil, fmt.Errorf("expected 1 jackpot, got %d", len(jackpot)) } txt, err := jackpot[0].Text() if err != nil { return nil, fmt.Errorf("failed to get jackpot: %w", err) } // jackpot is in the format of "$1.5 billion", "$100 million", or "$200,000" etc // make one filter to only get the numeric part of the jackpot numericOnly := func(in string) float64 { var out string for _, r := range in { if r >= '0' && r <= '9' { out += string(r) } if r == '.' { out += string(r) } } val, err := strconv.ParseFloat(out, 64) if err != nil { return 0 } return val } numeric := numericOnly(txt) set := false if strings.Contains(txt, "Billion") { amt := currency.USD.Amount(numeric * 1000000000) nextDrawing.Jackpot = amt set = true } else if strings.Contains(txt, "Million") { amt := currency.USD.Amount(numeric * 1000000) nextDrawing.Jackpot = amt set = true } else { amt := currency.USD.Amount(numeric) nextDrawing.Jackpot = amt set = true } if !set { return nil, fmt.Errorf("failed to convert jackpot to currency: %w", err) } return &nextDrawing, nil } func (c Config) GetCurrent(ctx context.Context, b extractor.Browser) (*Drawing, *NextDrawing, error) { c = c.validate() doc, err := b.Open(ctx, "https://www.megamillions.com/", extractor.OpenPageOptions{}) if err != nil { return nil, nil, err } defer deferClose(doc) d, err := getDrawing(ctx, doc) if err != nil { return nil, nil, err } nd, err := getNextDrawing(ctx, doc) if err != nil { return nil, nil, err } return d, nd, nil } func GetCurrent(ctx context.Context, b extractor.Browser) (*Drawing, *NextDrawing, error) { return DefaultConfig.GetCurrent(ctx, b) }