Files
u-desktop/weather.go

362 lines
8.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"regexp"
"strings"
"sync"
"time"
)
const qweatherHost = "https://pb4nmv4qnu.re.qweatherapi.com"
type City struct {
ID string `json:"id"`
Name string `json:"name"`
Adm2 string `json:"adm2"`
Adm1 string `json:"adm1"`
}
var cities = []City{
{"101010100", "北京", "北京", "北京"},
{"101020100", "上海", "上海", "上海"},
{"101280101", "广州", "广州", "广东"},
{"101280601", "深圳", "深圳", "广东"},
{"101200101", "武汉", "武汉", "湖北"},
{"101110101", "西安", "西安", "陕西"},
{"101210101", "杭州", "杭州", "浙江"},
{"101190101", "南京", "南京", "江苏"},
{"101070101", "沈阳", "沈阳", "辽宁"},
{"101050101", "哈尔滨", "哈尔滨", "黑龙江"},
{"101250101", "长沙", "长沙", "湖南"},
{"101270101", "成都", "成都", "四川"},
{"101090101", "石家庄", "石家庄", "河北"},
{"101090206", "任丘", "任丘", "河北"},
{"101090301", "邯郸", "邯郸", "河北"},
{"101290106", "宣威", "宣威", "云南"},
{"101290101", "昆明", "昆明", "云南"},
{"101260101", "贵阳", "贵阳", "贵州"},
}
var defaultCity = City{"101200101", "武汉", "武汉", "湖北"}
var httpClient = &http.Client{Timeout: 10 * time.Second}
var weatherIcons = map[string]string{
"晴": "☀️", "多云": "⛅", "阴": "☁️",
"雨": "🌧️", "雪": "❄️", "雷": "⛈️",
"雾": "🌫️", "霾": "😷", "风": "💨",
}
func getWeatherIcon(text string) string {
for k, v := range weatherIcons {
if strings.Contains(text, k) {
return v
}
}
return "🌤️"
}
func httpGet(url string) ([]byte, error) {
resp, err := httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// ── 定位 ──
func getLocation() City {
if city := locateByIPIP(); city != nil {
return *city
}
if city := locateByGeoAPI(); city != nil {
return *city
}
log.Println("所有定位失败,使用默认城市:", defaultCity.Name)
return defaultCity
}
var (
reIPIP = regexp.MustCompile(`来自于[:]\s*(.+?)\s*$`)
reIPAddr = regexp.MustCompile(`(\d+\.\d+\.\d+\.\d+)`)
)
func locateByIPIP() *City {
data, err := httpGet("https://myip.ipip.net")
if err != nil {
return nil
}
text := string(data)
m := reIPIP.FindStringSubmatch(text)
if m == nil {
return nil
}
parts := strings.Fields(m[1])
cityName := parts[0]
if len(parts) >= 3 {
cityName = parts[2]
} else if len(parts) >= 2 {
cityName = parts[1]
}
for _, c := range cities {
if c.Name == cityName {
log.Println("ipip.net 匹配:", c.Name)
return &c
}
}
return nil
}
func locateByGeoAPI() *City {
data, err := httpGet("https://myip.ipip.net")
if err == nil {
if m := reIPAddr.FindStringSubmatch(string(data)); m != nil {
return geoLookup(m[1])
}
}
return nil
}
func geoLookup(ip string) *City {
key := loadConfig().qweatherKey()
if key == "" {
log.Println("未配置和风天气 API Key")
return nil
}
url := fmt.Sprintf(qweatherHost+"/v2/city/lookup?location=%s&key=%s", ip, key)
data, err := httpGet(url)
if err != nil {
return nil
}
var resp struct {
Code string `json:"code"`
Location []struct {
ID string `json:"id"`
Name string `json:"name"`
Adm2 string `json:"adm2"`
Adm1 string `json:"adm1"`
} `json:"location"`
}
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" || len(resp.Location) == 0 {
return nil
}
loc := resp.Location[0]
for _, c := range cities {
if c.Name == loc.Name {
return &c
}
}
return &City{loc.ID, loc.Name, loc.Adm2, loc.Adm1}
}
func getCurrentCity() City {
cfg := loadConfig()
if cfg.City != "" {
for _, c := range cities {
if c.ID == cfg.City {
return c
}
}
}
return getLocation()
}
// ── 天气数据 ──
type hourlyItem struct {
Time string `json:"time"`
Temp string `json:"temp"`
Icon string `json:"icon"`
Pop string `json:"pop,omitempty"`
}
type dailyItem struct {
Date string `json:"date"`
Icon string `json:"icon"`
TempMin string `json:"tempMin"`
TempMax string `json:"tempMax"`
}
type currentWeather struct {
Text string `json:"text"`
Temp string `json:"temp"`
}
var (
weatherCacheMu sync.Mutex
weatherCache map[string]time.Time
weatherCacheData map[string]string
)
func init() {
weatherCache = make(map[string]time.Time)
weatherCacheData = make(map[string]string)
}
func fetchAndPushWeather(city City) {
// 5 分钟内同一城市不重复请求
weatherCacheMu.Lock()
if last, ok := weatherCache[city.ID]; ok && time.Since(last) < 5*time.Minute {
if cached, ok := weatherCacheData[city.ID]; ok {
weatherCacheMu.Unlock()
evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, cached))
return
}
}
weatherCacheMu.Unlock()
type weatherData struct {
Current string `json:"current"`
Hourly []hourlyItem `json:"hourly"`
Daily []dailyItem `json:"daily"`
City string `json:"city"`
}
wd := weatherData{}
cityName := city.Name
if city.Adm2 != city.Name {
cityName = city.Adm2 + " " + city.Name
}
wd.City = cityName
if now := fetchCurrentWeather(city.ID); now != nil {
icon := getWeatherIcon(now.Text)
wd.Current = fmt.Sprintf("📍 %s %s %s %s°C", cityName, icon, now.Text, now.Temp)
} else {
wd.Current = fmt.Sprintf("📍 %s ☀️ 晴 24°C", cityName)
}
wd.Hourly = fetchHourlyForecast(city.ID)
wd.Daily = fetchDailyForecast(city.ID)
jsonData, _ := json.Marshal(wd)
weatherCacheMu.Lock()
weatherCache[city.ID] = time.Now()
weatherCacheData[city.ID] = string(jsonData)
weatherCacheMu.Unlock()
evalJS(fmt.Sprintf(`if(window.updateWeatherFromGo) window.updateWeatherFromGo(%s)`, string(jsonData)))
log.Println("天气已推送:", wd.Current)
}
func fetchCurrentWeather(cityID string) *currentWeather {
key := loadConfig().qweatherKey()
if key == "" {
log.Println("未配置和风天气 API Key")
return nil
}
url := fmt.Sprintf(qweatherHost+"/v7/weather/now?location=%s&key=%s", cityID, key)
data, err := httpGet(url)
if err != nil {
return nil
}
var resp struct {
Code string `json:"code"`
Now struct {
Text string `json:"text"`
Temp string `json:"temp"`
} `json:"now"`
}
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
return nil
}
return &currentWeather{resp.Now.Text, resp.Now.Temp}
}
func fetchHourlyForecast(cityID string) []hourlyItem {
key := loadConfig().qweatherKey()
if key == "" {
return nil
}
url := fmt.Sprintf(qweatherHost+"/v7/weather/24h?location=%s&key=%s", cityID, key)
data, err := httpGet(url)
if err != nil {
return nil
}
var resp struct {
Code string `json:"code"`
Hourly []struct {
FxTime string `json:"fxTime"`
Temp string `json:"temp"`
Pop string `json:"pop"`
Text string `json:"text"`
} `json:"hourly"`
}
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
return nil
}
var result []hourlyItem
for i, h := range resp.Hourly {
if i >= 8 {
break
}
t := h.FxTime
if idx := strings.Index(t, "T"); idx >= 0 {
t = t[idx+1:]
if len(t) >= 5 {
t = t[:5]
}
}
result = append(result, hourlyItem{t, h.Temp + "°", getWeatherIcon(h.Text), h.Pop})
}
return result
}
func fetchDailyForecast(cityID string) []dailyItem {
key := loadConfig().qweatherKey()
if key == "" {
return nil
}
url := fmt.Sprintf(qweatherHost+"/v7/weather/7d?location=%s&key=%s", cityID, key)
data, err := httpGet(url)
if err != nil {
return nil
}
var resp struct {
Code string `json:"code"`
Daily []struct {
FxDate string `json:"fxDate"`
TextDay string `json:"textDay"`
TempMin string `json:"tempMin"`
TempMax string `json:"tempMax"`
} `json:"daily"`
}
if json.Unmarshal(data, &resp) != nil || resp.Code != "200" {
return nil
}
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
today := time.Now().Format("2006-01-02")
var result []dailyItem
for _, d := range resp.Daily {
date := d.FxDate
if date == today {
date = "今天"
} else {
if t, err := time.Parse("2006-01-02", date); err == nil {
date = weekdays[t.Weekday()]
}
}
result = append(result, dailyItem{date, getWeatherIcon(d.TextDay), d.TempMin, d.TempMax})
}
return result
}
func weatherLoop() {
time.Sleep(3 * time.Second)
city := getCurrentCity()
log.Println("初始城市:", city.Name)
fetchAndPushWeather(city)
ticker := time.NewTicker(10 * time.Minute)
for range ticker.C {
city := getCurrentCity()
fetchAndPushWeather(city)
}
}