package main import ( "context" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "strings" "time" ) const radarCacheTTL = 10 * time.Minute func requireMPV() error { if _, err := exec.LookPath("mpv"); err != nil { return fmt.Errorf("mpv is required for radar mode") } return nil } func runRadar(ctx context.Context, cache *Cache, loc Location, searchQuery string, userAgent string) error { station := strings.TrimSpace(loc.RadarStation) if station == "" { client := NewNWSClient(userAgent) var err error station, err = client.FetchRadarStation(ctx, loc) if err != nil { return err } loc.RadarStation = station if err := cache.UpdateLocation(loc); err != nil { if searchQuery == "" { return err } if err := cache.SaveLocation(searchQuery, loc); err != nil { return err } } } url, err := radarURL(station) if err != nil { return err } radarPath, err := cacheRadarGIF(ctx, cache, url, userAgent) if err != nil { return err } cmd := exec.CommandContext(ctx, "mpv", "--fs", "--loop-file", radarPath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin return cmd.Run() } func radarURL(station string) (string, error) { station = strings.TrimSpace(station) if station == "" { return "", fmt.Errorf("radar station not set") } station = strings.ToUpper(station) return fmt.Sprintf("https://radar.weather.gov/ridge/standard/%s_loop.gif", station), nil } func cacheRadarGIF(ctx context.Context, cache *Cache, url, userAgent string) (string, error) { if err := cache.ensureDir(); err != nil { return "", err } path := filepath.Join(cache.dir, "radar.gif") if !shouldRefreshFile(path, radarCacheTTL) { return path, nil } req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", err } req.Header.Set("User-Agent", userAgent) client := &http.Client{Timeout: 20 * time.Second} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode > 299 { return "", fmt.Errorf("radar request failed with status %d", resp.StatusCode) } tmpPath := path + ".tmp" tmp, err := os.Create(tmpPath) if err != nil { return "", err } _, copyErr := io.Copy(tmp, resp.Body) closeErr := tmp.Close() if copyErr != nil { _ = os.Remove(tmpPath) return "", copyErr } if closeErr != nil { _ = os.Remove(tmpPath) return "", closeErr } if err := os.Rename(tmpPath, path); err != nil { _ = os.Remove(tmpPath) return "", err } return path, nil } func shouldRefreshFile(path string, ttl time.Duration) bool { info, err := os.Stat(path) if err != nil { return true } return time.Since(info.ModTime()) > ttl }