weather/radar.go

127 lines
2.7 KiB
Go

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
}