/* * Scope - A simple and minimal metasearch engine * Copyright (C) 2021 Arsen Musayelyan * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package cards import ( "encoding/json" "fmt" "html/template" "net/http" "net/url" "strconv" "strings" "time" "github.com/bcicen/go-units" "github.com/spf13/viper" ) func init() { Register("metaweather", 1, NewMetaweatherCard) } const metaweatherContent = `

%s

%s

Min Temp: %s

Max Temp: %s

Wind Speed: %s

Humidity: %d%%

Visibility: %s

Predictability: %d%%

Wind Direction: %s

Location: %s

Timezone: %s

` // weatherIconMap maps metaweather abbreviations // to iconify icons var weatherIconMap = map[string]string{ "sn": "bi:cloud-snow", "sl": "bi:cloud-sleet", "h": "bi:cloud-hail", "t": "bi:cloud-lightning-rain", "hr": "bi:cloud-rain-heavy", "lr": "bi:cloud-drizzle", "s": "wi:day-rain", "hc": "bi:cloud-fill", "lc": "bi:cloud", "c": "akar-icons:sun", } // MetaweatherCard represents a metaweather card type MetaweatherCard struct { query string userAgent string location []MetaweatherLocation resp MetaweatherResponse } // NewMetaweatherCard is a NewCardFunc that creates a new MetaweatherCard func NewMetaweatherCard(query, userAgent string) Card { return &MetaweatherCard{ query: query, userAgent: userAgent, } } // MetaweatherLocation represents a location type MetaweatherLocation struct { Title string `json:"title"` WOEID int `json:"woeid"` } // MetaweatherResponse represents a response from // the metaweather API type MetaweatherResponse struct { Detail string `json:"detail"` Title string `json:"title"` Timezone string `json:"timezone"` Consolidated []struct { ID int `json:"id"` State string `json:"weather_state_name"` StateAbbr string `json:"weather_state_abbr"` WindCompass string `json:"wind_direction_compass"` MinTemp float64 `json:"min_temp"` MaxTemp float64 `json:"max_temp"` CurrentTemp float64 `json:"the_temp"` WindSpeed float64 `json:"wind_speed"` WindDirection float64 `json:"wind_direction"` AirPressure float64 `json:"air_pressure"` Visibility float64 `json:"visibility"` Humidity int `json:"humidity"` Predictability int `json:"predictability"` } `json:"consolidated_weather"` } // RunQuery searches for the location and then runs an API query // using the returned WOEID func (mc *MetaweatherCard) RunQuery() error { http.DefaultClient.Timeout = 5 * time.Second // Create location search request req, err := http.NewRequest( http.MethodGet, "https://www.metaweather.com/api/location/search/?query="+url.QueryEscape(mc.StripKey()), nil, ) if err != nil { return err } req.Header.Set("User-Agent", mc.userAgent) // Perform request res, err := http.DefaultClient.Do(req) if err != nil { return err } // Decode response into location err = json.NewDecoder(res.Body).Decode(&mc.location) if err != nil { return err } res.Body.Close() // If no locations, search returned no results if len(mc.location) == 0 { return nil } // Create request for weather data req, err = http.NewRequest( http.MethodGet, "https://www.metaweather.com/api/location/"+strconv.Itoa(mc.location[0].WOEID), nil, ) if err != nil { return err } req.Header.Set("User-Agent", mc.userAgent) // Perform request res, err = http.DefaultClient.Do(req) if err != nil { return err } // Decode response into response struct err = json.NewDecoder(res.Body).Decode(&mc.resp) if err != nil { return err } return nil } // Returned checks whether no location was found or response // said not found. func (mc *MetaweatherCard) Returned() bool { return len(mc.location) > 0 && mc.resp.Detail != "Not found." } // Matches determines whether the query matches the keys for // MetaweatherCard func (mc *MetaweatherCard) Matches() bool { return strings.HasPrefix(mc.query, "weather in") || strings.HasPrefix(mc.query, "weather") || strings.HasSuffix(mc.query, "weather") } // StripKey removes keys related to MetaweatherCard func (mc *MetaweatherCard) StripKey() string { query := strings.TrimPrefix(mc.query, "weather in") query = strings.TrimPrefix(query, "weather") query = strings.TrimSuffix(query, "weather") return strings.TrimSpace(query) } // Content returns metaweatherContent with all information added func (mc *MetaweatherCard) Content() template.HTML { return template.HTML(fmt.Sprintf( metaweatherContent, // Write iconift icon for weather state weatherIconMap[mc.resp.Consolidated[0].StateAbbr], // Write weather state mc.resp.Consolidated[0].State, // Convert and write current temperature convert(mc.resp.Consolidated[0].CurrentTemp, "temperature", units.Celsius), // Convert and write minimum temperature convert(mc.resp.Consolidated[0].MinTemp, "temperature", units.Celsius), // Convert and write maximum temperature convert(mc.resp.Consolidated[0].MaxTemp, "temperature", units.Celsius), // Write wind speed fmt.Sprintf("%.2f mph", mc.resp.Consolidated[0].WindSpeed), // Write humidity percentafe mc.resp.Consolidated[0].Humidity, // Convert and write visibility convert(mc.resp.Consolidated[0].Visibility, "visibility", units.Mile), // Write predictability percentage mc.resp.Consolidated[0].Predictability, // Write compass wind direction mc.resp.Consolidated[0].WindCompass, // Write title mc.resp.Title, // Write timezone mc.resp.Timezone, )) } // Title of MetaweatherCard is "Weather" func (mc *MetaweatherCard) Title() string { return "Weather" } // Footer returns a footer with a link to metaweather func (mc *MetaweatherCard) Footer() template.HTML { return `` } // Head returns an empty string as no extra head tags // are required for MetaweatherCard func (mc *MetaweatherCard) Head() template.HTML { return "" } // convert converts a value to a unit specified in the config, and then formats it func convert(val float64, suffix string, defaultUnit units.Unit) string { // Find unit in config unit, err := units.Find(viper.GetString("search.cards.weather.units." + suffix)) if err != nil { unit = defaultUnit } // Convert value to specified unit newVal, err := units.ConvertFloat(val, defaultUnit, unit) opts := units.FmtOptions{ Label: true, Short: true, Precision: 2, } if err != nil { return units.NewValue(val, defaultUnit).Fmt(opts) } return newVal.Fmt(opts) }