127 lines
2.7 KiB
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
|
|
}
|