Files
box-box/cmd/main.go

188 lines
4.8 KiB
Go
Raw Normal View History

2026-03-03 00:48:05 -05:00
package main
import (
2026-05-25 12:30:21 -04:00
"context"
2026-03-27 20:12:57 -04:00
"flag"
2026-03-03 00:48:05 -05:00
"fmt"
2026-03-27 02:54:09 -04:00
"log"
2026-05-25 12:30:21 -04:00
"net/http"
2026-03-03 00:48:05 -05:00
"os"
"time"
"github.com/AmanTahiliani/box-box/internal/api"
2026-05-25 00:45:46 -04:00
"github.com/AmanTahiliani/box-box/internal/ingest"
2026-05-25 12:30:21 -04:00
"github.com/AmanTahiliani/box-box/internal/news"
2026-05-25 00:45:46 -04:00
"github.com/AmanTahiliani/box-box/internal/store"
2026-03-03 00:48:05 -05:00
"github.com/AmanTahiliani/box-box/internal/ui"
2026-03-27 20:12:57 -04:00
"github.com/AmanTahiliani/box-box/internal/web"
2026-03-03 00:48:05 -05:00
tea "github.com/charmbracelet/bubbletea"
)
func main() {
2026-03-27 20:12:57 -04:00
webMode := flag.Bool("web", false, "Start web companion server instead of TUI")
port := flag.Int("port", 8080, "Port for web server (used with --web)")
2026-05-25 00:45:46 -04:00
ingestYear := flag.Int("ingest-year", 0, "Ingest OpenF1 meetings for a season year")
2026-05-25 02:38:27 -04:00
ingestMeeting := flag.Int("ingest-meeting", 0, "Ingest meeting metadata and Race Hub datasets for all sessions")
2026-05-25 00:45:46 -04:00
ingestSession := flag.Int("ingest-session", 0, "Ingest Race Hub datasets for a session key")
2026-05-25 12:30:21 -04:00
ingestNews := flag.Bool("ingest-news", false, "Refresh RSS/Atom paddock briefing feeds")
2026-05-25 00:45:46 -04:00
dryRun := flag.Bool("dry-run", false, "Preview ingestion without writing domain rows")
dbPath := flag.String("db", "", "Domain database path (default: ~/.local/share/box-box/boxbox.db)")
2026-03-27 20:12:57 -04:00
flag.Parse()
2026-03-27 02:54:09 -04:00
2026-03-27 00:30:53 -04:00
var client *api.OpenF1Client
if apiKey := os.Getenv("OPENF1_API_KEY"); apiKey != "" {
client = api.NewOpenF1ClientWithKey("https://api.openf1.org", 15*time.Second, apiKey)
} else {
client = api.NewOpenF1Client("https://api.openf1.org", 15*time.Second)
}
defer client.Close()
// Clean up old file-based cache (one-time migration).
go api.CleanupOldFileCache()
2026-05-25 00:45:46 -04:00
ingestFlags := 0
if *ingestYear != 0 {
ingestFlags++
}
if *ingestMeeting != 0 {
ingestFlags++
}
if *ingestSession != 0 {
ingestFlags++
}
2026-05-25 12:30:21 -04:00
if *ingestNews {
ingestFlags++
}
2026-05-25 00:45:46 -04:00
if ingestFlags > 0 {
if ingestFlags > 1 {
2026-05-25 12:30:21 -04:00
fmt.Fprintln(os.Stderr, "box-box: only one of --ingest-year, --ingest-meeting, --ingest-session, or --ingest-news may be set")
2026-05-25 00:45:46 -04:00
os.Exit(1)
}
2026-05-25 12:30:21 -04:00
if *ingestNews {
if err := runNewsIngestion(*dryRun, *dbPath); err != nil {
fmt.Fprintf(os.Stderr, "box-box ingest error: %v\n", err)
os.Exit(1)
}
return
}
2026-05-25 00:45:46 -04:00
if err := runIngestion(client, *ingestYear, *ingestMeeting, *ingestSession, *dryRun, *dbPath); err != nil {
fmt.Fprintf(os.Stderr, "box-box ingest error: %v\n", err)
os.Exit(1)
}
return
}
2026-03-27 20:12:57 -04:00
if *webMode {
log.SetOutput(os.Stderr) // web mode logs to stderr, not file
fmt.Printf("box-box web → http://localhost:%d\n", *port)
2026-05-25 00:49:13 -04:00
var domainStore *store.Store
db := *dbPath
if db == "" {
db = store.DefaultDBPath()
}
if st, err := store.Open(db); err != nil {
log.Printf("web: domain database unavailable (%s): %v", db, err)
} else {
domainStore = st
defer domainStore.Close()
}
srv := web.NewServer(client, *port, domainStore)
2026-03-27 20:12:57 -04:00
log.Fatal(srv.Start())
return
}
// TUI mode: redirect logs to file so they don't pollute the terminal.
if f, err := os.OpenFile("box-box.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644); err == nil {
log.SetOutput(f)
defer f.Close()
}
2026-03-03 00:48:05 -05:00
model := ui.NewAppModel(client)
p := tea.NewProgram(
model,
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "box-box error: %v\n", err)
os.Exit(1)
}
}
2026-05-25 00:45:46 -04:00
func runIngestion(client *api.OpenF1Client, year, meetingKey, sessionKey int, dryRun bool, dbPath string) error {
log.SetOutput(os.Stderr)
path := dbPath
if path == "" {
path = store.DefaultDBPath()
}
st, err := store.Open(path)
if err != nil {
return fmt.Errorf("open domain database: %w", err)
}
defer st.Close()
opts := ingest.DefaultOptions()
opts.DryRun = dryRun
opts.Progress = ingest.NewProgress(os.Stderr)
svc := ingest.NewService(st, ingest.NewOpenF1Source(client), opts)
switch {
case year != 0:
_, err = svc.IngestYear(year)
case meetingKey != 0:
_, err = svc.IngestMeeting(meetingKey)
case sessionKey != 0:
_, err = svc.IngestSession(sessionKey)
}
return err
}
2026-05-25 12:30:21 -04:00
func runNewsIngestion(dryRun bool, dbPath string) error {
log.SetOutput(os.Stderr)
path := dbPath
if path == "" {
path = store.DefaultDBPath()
}
var st *store.Store
if !dryRun {
var err error
st, err = store.Open(path)
if err != nil {
return fmt.Errorf("open domain database: %w", err)
}
defer st.Close()
}
if dryRun {
fmt.Fprintf(os.Stderr, "news: dry run, not writing to %s\n", path)
} else {
fmt.Fprintf(os.Stderr, "news: refreshing feeds into %s\n", path)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
result, err := news.Refresh(ctx, st, news.RefreshOptions{
Client: &http.Client{Timeout: 10 * time.Second},
DryRun: dryRun,
Progress: os.Stderr,
})
fmt.Fprintf(
os.Stderr,
"news: %d source(s) fetched, %d failed, %d item(s) fetched, %d upserted\n",
result.SourcesFetched,
result.SourcesFailed,
result.ItemsFetched,
result.ItemsUpserted,
)
return err
}