mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
411 lines
13 KiB
Go
411 lines
13 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"fotbal-club/internal/models"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type WeatherService struct {
|
|
db *gorm.DB
|
|
apiKey string
|
|
baseURL string
|
|
client *http.Client
|
|
}
|
|
|
|
type WeatherResponse struct {
|
|
Location struct {
|
|
Name string `json:"name"`
|
|
Region string `json:"region"`
|
|
Country string `json:"country"`
|
|
Lat float64 `json:"lat"`
|
|
Lon float64 `json:"lon"`
|
|
TzID string `json:"tz_id"`
|
|
LocalTime string `json:"localtime"`
|
|
} `json:"location"`
|
|
Current struct {
|
|
LastUpdated string `json:"last_updated"`
|
|
TempC float64 `json:"temp_c"`
|
|
TempF float64 `json:"temp_f"`
|
|
IsDay int `json:"is_day"`
|
|
Condition struct {
|
|
Text string `json:"text"`
|
|
Icon string `json:"icon"`
|
|
Code int `json:"code"`
|
|
} `json:"condition"`
|
|
WindMph float64 `json:"wind_mph"`
|
|
WindKph float64 `json:"wind_kph"`
|
|
WindDegree int `json:"wind_degree"`
|
|
WindDir string `json:"wind_dir"`
|
|
PressureMb float64 `json:"pressure_mb"`
|
|
PressureIn float64 `json:"pressure_in"`
|
|
PrecipMm float64 `json:"precip_mm"`
|
|
PrecipIn float64 `json:"precip_in"`
|
|
Humidity int `json:"humidity"`
|
|
Cloud int `json:"cloud"`
|
|
FeelsLikeC float64 `json:"feelslike_c"`
|
|
FeelsLikeF float64 `json:"feelslike_f"`
|
|
VisKm float64 `json:"vis_km"`
|
|
VisMiles float64 `json:"vis_miles"`
|
|
UV float64 `json:"uv"`
|
|
GustMph float64 `json:"gust_mph"`
|
|
GustKph float64 `json:"gust_kph"`
|
|
} `json:"current"`
|
|
Forecast struct {
|
|
ForecastDay []struct {
|
|
Date string `json:"date"`
|
|
DateEpoch int64 `json:"date_epoch"`
|
|
Day struct {
|
|
MaxTempC float64 `json:"maxtemp_c"`
|
|
MaxTempF float64 `json:"maxtemp_f"`
|
|
MinTempC float64 `json:"mintemp_c"`
|
|
MinTempF float64 `json:"mintemp_f"`
|
|
AvgTempC float64 `json:"avgtemp_c"`
|
|
AvgTempF float64 `json:"avgtemp_f"`
|
|
MaxWindMph float64 `json:"maxwind_mph"`
|
|
MaxWindKph float64 `json:"maxwind_kph"`
|
|
TotalPrecipMm float64 `json:"totalprecip_mm"`
|
|
TotalPrecipIn float64 `json:"totalprecip_in"`
|
|
TotalSnowCm float64 `json:"totalsnow_cm"`
|
|
AvgVisKm float64 `json:"avgvis_km"`
|
|
AvgVisMiles float64 `json:"avgvis_miles"`
|
|
AvgHumidity float64 `json:"avghumidity"`
|
|
DailyWillItRain int `json:"daily_will_it_rain"`
|
|
DailyChanceOfRain int `json:"daily_chance_of_rain"`
|
|
DailyWillItSnow int `json:"daily_will_it_snow"`
|
|
DailyChanceOfSnow int `json:"daily_chance_of_snow"`
|
|
Condition struct {
|
|
Text string `json:"text"`
|
|
Icon string `json:"icon"`
|
|
Code int `json:"code"`
|
|
} `json:"condition"`
|
|
UV float64 `json:"uv"`
|
|
} `json:"day"`
|
|
Astro struct {
|
|
Sunrise string `json:"sunrise"`
|
|
Sunset string `json:"sunset"`
|
|
Moonrise string `json:"moonrise"`
|
|
Moonset string `json:"moonset"`
|
|
MoonPhase string `json:"moon_phase"`
|
|
MoonIllumination float64 `json:"moon_illumination"`
|
|
IsMoonUp int `json:"is_moon_up"`
|
|
IsSunUp int `json:"is_sun_up"`
|
|
} `json:"astro"`
|
|
Hour []struct {
|
|
TimeEpoch int64 `json:"time_epoch"`
|
|
Time string `json:"time"`
|
|
TempC float64 `json:"temp_c"`
|
|
TempF float64 `json:"temp_f"`
|
|
IsDay int `json:"is_day"`
|
|
Condition struct {
|
|
Text string `json:"text"`
|
|
Icon string `json:"icon"`
|
|
Code int `json:"code"`
|
|
} `json:"condition"`
|
|
WindMph float64 `json:"wind_mph"`
|
|
WindKph float64 `json:"wind_kph"`
|
|
WindDegree int `json:"wind_degree"`
|
|
WindDir string `json:"wind_dir"`
|
|
PressureMb float64 `json:"pressure_mb"`
|
|
PressureIn float64 `json:"pressure_in"`
|
|
PrecipMm float64 `json:"precip_mm"`
|
|
PrecipIn float64 `json:"precip_in"`
|
|
Humidity int `json:"humidity"`
|
|
Cloud int `json:"cloud"`
|
|
FeelsLikeC float64 `json:"feelslike_c"`
|
|
FeelsLikeF float64 `json:"feelslike_f"`
|
|
WindChillC float64 `json:"windchill_c"`
|
|
WindChillF float64 `json:"windchill_f"`
|
|
HeatIndexC float64 `json:"heatindex_c"`
|
|
HeatIndexF float64 `json:"heatindex_f"`
|
|
DewPointC float64 `json:"dewpoint_c"`
|
|
DewPointF float64 `json:"dewpoint_f"`
|
|
WillItRain int `json:"will_it_rain"`
|
|
ChanceOfRain int `json:"chance_of_rain"`
|
|
WillItSnow int `json:"will_it_snow"`
|
|
ChanceOfSnow int `json:"chance_of_snow"`
|
|
VisKm float64 `json:"vis_km"`
|
|
VisMiles float64 `json:"vis_miles"`
|
|
GustMph float64 `json:"gust_mph"`
|
|
GustKph float64 `json:"gust_kph"`
|
|
UV float64 `json:"uv"`
|
|
} `json:"hour"`
|
|
} `json:"forecastday"`
|
|
} `json:"forecast"`
|
|
Alerts []struct {
|
|
Headline string `json:"headline"`
|
|
MsgType string `json:"msgtype"`
|
|
Severity string `json:"severity"`
|
|
Urgency string `json:"urgency"`
|
|
Areas string `json:"areas"`
|
|
Category string `json:"category"`
|
|
Certainty string `json:"certainty"`
|
|
Event string `json:"event"`
|
|
Note string `json:"note"`
|
|
Effective string `json:"effective"`
|
|
Expires string `json:"expires"`
|
|
Desc string `json:"desc"`
|
|
Instruction string `json:"instruction"`
|
|
} `json:"alerts"`
|
|
}
|
|
|
|
func NewWeatherService(db *gorm.DB) *WeatherService {
|
|
apiKey := os.Getenv("WEATHER_API_KEY")
|
|
baseURL := os.Getenv("WEATHER_API_BASE_URL")
|
|
|
|
if apiKey == "" {
|
|
apiKey = "20dfd9a556ec43888dc103523250904" // fallback
|
|
}
|
|
if baseURL == "" {
|
|
baseURL = "https://api.weatherapi.com/v1"
|
|
}
|
|
|
|
return &WeatherService{
|
|
db: db,
|
|
apiKey: apiKey,
|
|
baseURL: baseURL,
|
|
client: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (ws *WeatherService) GetWeatherByLocation(location string) (*WeatherResponse, error) {
|
|
if location == "" {
|
|
// Try to get club location from settings
|
|
location = ws.getClubLocation()
|
|
if location == "" {
|
|
return nil, fmt.Errorf("no location specified and no club location found")
|
|
}
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/forecast.json?key=%s&q=%s&days=3&aqi=no&alerts=no",
|
|
ws.baseURL, ws.apiKey, location)
|
|
|
|
resp, err := ws.client.Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch weather data: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("weather API error: status %d, response: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
var weatherResp WeatherResponse
|
|
if err := json.Unmarshal(body, &weatherResp); err != nil {
|
|
return nil, fmt.Errorf("failed to parse weather response: %w", err)
|
|
}
|
|
|
|
return &weatherResp, nil
|
|
}
|
|
|
|
func (ws *WeatherService) getClubLocation() string {
|
|
var settings models.Settings
|
|
if err := ws.db.First(&settings).Error; err != nil {
|
|
return ""
|
|
}
|
|
|
|
// If coordinates are available, use them for most accurate weather
|
|
if settings.LocationLatitude != 0 && settings.LocationLongitude != 0 {
|
|
return fmt.Sprintf("%.6f,%.6f", settings.LocationLatitude, settings.LocationLongitude)
|
|
}
|
|
|
|
// Try different location fields in order of preference
|
|
if settings.ContactCity != "" {
|
|
location := settings.ContactCity
|
|
if settings.ContactCountry != "" &&
|
|
settings.ContactCountry != "Czech Republic" &&
|
|
settings.ContactCountry != "Česká republika" &&
|
|
settings.ContactCountry != "Česko" {
|
|
location += "," + settings.ContactCountry
|
|
}
|
|
return location
|
|
}
|
|
|
|
// Fallback to a default Czech city if club is in Czech Republic
|
|
if settings.ContactCountry == "Czech Republic" ||
|
|
settings.ContactCountry == "Česká republika" ||
|
|
settings.ContactCountry == "Česko" {
|
|
return "Prague"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (ws *WeatherService) GetWeatherForClub() (*WeatherResponse, error) {
|
|
return ws.GetWeatherByLocation("")
|
|
}
|
|
|
|
func (ws *WeatherService) GetWeatherForMatch(matchDateTime string, location string) (*WeatherResponse, error) {
|
|
if location == "" {
|
|
location = ws.getClubLocation()
|
|
if location == "" {
|
|
return nil, fmt.Errorf("no location specified and no club location found")
|
|
}
|
|
}
|
|
|
|
// Parse match date to determine if we need hourly forecast
|
|
matchTime, err := time.Parse("2006-01-02T15:04:05", matchDateTime)
|
|
if err != nil {
|
|
// Try alternative format
|
|
matchTime, err = time.Parse("2006-01-02 15:04:05", matchDateTime)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid match date time format: %v", err)
|
|
}
|
|
}
|
|
|
|
// If match is more than 7 days away, regular forecast won't be available
|
|
now := time.Now()
|
|
if matchTime.Sub(now) > 7*24*time.Hour {
|
|
return nil, fmt.Errorf("match is too far in the future for weather forecast")
|
|
}
|
|
|
|
// If match is in the past, no forecast needed
|
|
if matchTime.Before(now) {
|
|
return nil, fmt.Errorf("match is in the past")
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/forecast.json?key=%s&q=%s&days=7&aqi=no&alerts=no",
|
|
ws.baseURL, ws.apiKey, location)
|
|
|
|
resp, err := ws.client.Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch weather data: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("weather API error: status %d, response: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
var weatherResp WeatherResponse
|
|
if err := json.Unmarshal(body, &weatherResp); err != nil {
|
|
return nil, fmt.Errorf("failed to parse weather response: %w", err)
|
|
}
|
|
|
|
return &weatherResp, nil
|
|
}
|
|
|
|
func (ws *WeatherService) FindClosestHourlyForecast(weather *WeatherResponse, matchTime time.Time) *struct {
|
|
TimeEpoch int64 `json:"time_epoch"`
|
|
Time string `json:"time"`
|
|
TempC float64 `json:"temp_c"`
|
|
TempF float64 `json:"temp_f"`
|
|
IsDay int `json:"is_day"`
|
|
Condition struct {
|
|
Text string `json:"text"`
|
|
Icon string `json:"icon"`
|
|
Code int `json:"code"`
|
|
} `json:"condition"`
|
|
WindMph float64 `json:"wind_mph"`
|
|
WindKph float64 `json:"wind_kph"`
|
|
WindDegree int `json:"wind_degree"`
|
|
WindDir string `json:"wind_dir"`
|
|
PressureMb float64 `json:"pressure_mb"`
|
|
PressureIn float64 `json:"pressure_in"`
|
|
PrecipMm float64 `json:"precip_mm"`
|
|
PrecipIn float64 `json:"precip_in"`
|
|
Humidity int `json:"humidity"`
|
|
Cloud int `json:"cloud"`
|
|
FeelsLikeC float64 `json:"feelslike_c"`
|
|
FeelsLikeF float64 `json:"feelslike_f"`
|
|
WindChillC float64 `json:"windchill_c"`
|
|
WindChillF float64 `json:"windchill_f"`
|
|
HeatIndexC float64 `json:"heatindex_c"`
|
|
HeatIndexF float64 `json:"heatindex_f"`
|
|
DewPointC float64 `json:"dewpoint_c"`
|
|
DewPointF float64 `json:"dewpoint_f"`
|
|
WillItRain int `json:"will_it_rain"`
|
|
ChanceOfRain int `json:"chance_of_rain"`
|
|
WillItSnow int `json:"will_it_snow"`
|
|
ChanceOfSnow int `json:"chance_of_snow"`
|
|
VisKm float64 `json:"vis_km"`
|
|
VisMiles float64 `json:"vis_miles"`
|
|
GustMph float64 `json:"gust_mph"`
|
|
GustKph float64 `json:"gust_kph"`
|
|
UV float64 `json:"uv"`
|
|
} {
|
|
var closestHour *struct {
|
|
TimeEpoch int64 `json:"time_epoch"`
|
|
Time string `json:"time"`
|
|
TempC float64 `json:"temp_c"`
|
|
TempF float64 `json:"temp_f"`
|
|
IsDay int `json:"is_day"`
|
|
Condition struct {
|
|
Text string `json:"text"`
|
|
Icon string `json:"icon"`
|
|
Code int `json:"code"`
|
|
} `json:"condition"`
|
|
WindMph float64 `json:"wind_mph"`
|
|
WindKph float64 `json:"wind_kph"`
|
|
WindDegree int `json:"wind_degree"`
|
|
WindDir string `json:"wind_dir"`
|
|
PressureMb float64 `json:"pressure_mb"`
|
|
PressureIn float64 `json:"pressure_in"`
|
|
PrecipMm float64 `json:"precip_mm"`
|
|
PrecipIn float64 `json:"precip_in"`
|
|
Humidity int `json:"humidity"`
|
|
Cloud int `json:"cloud"`
|
|
FeelsLikeC float64 `json:"feelslike_c"`
|
|
FeelsLikeF float64 `json:"feelslike_f"`
|
|
WindChillC float64 `json:"windchill_c"`
|
|
WindChillF float64 `json:"windchill_f"`
|
|
HeatIndexC float64 `json:"heatindex_c"`
|
|
HeatIndexF float64 `json:"heatindex_f"`
|
|
DewPointC float64 `json:"dewpoint_c"`
|
|
DewPointF float64 `json:"dewpoint_f"`
|
|
WillItRain int `json:"will_it_rain"`
|
|
ChanceOfRain int `json:"chance_of_rain"`
|
|
WillItSnow int `json:"will_it_snow"`
|
|
ChanceOfSnow int `json:"chance_of_snow"`
|
|
VisKm float64 `json:"vis_km"`
|
|
VisMiles float64 `json:"vis_miles"`
|
|
GustMph float64 `json:"gust_mph"`
|
|
GustKph float64 `json:"gust_kph"`
|
|
UV float64 `json:"uv"`
|
|
}
|
|
|
|
minDiff := 24 * time.Hour // Start with a large difference
|
|
|
|
for _, day := range weather.Forecast.ForecastDay {
|
|
for _, hour := range day.Hour {
|
|
hourTime := time.Unix(hour.TimeEpoch, 0)
|
|
diff := hourTime.Sub(matchTime)
|
|
if diff >= 0 && diff < minDiff {
|
|
minDiff = diff
|
|
closestHour = &hour
|
|
}
|
|
}
|
|
}
|
|
|
|
return closestHour
|
|
}
|
|
|
|
func (ws *WeatherService) GetWeatherIconURL(icon string) string {
|
|
if icon == "" {
|
|
return ""
|
|
}
|
|
// WeatherAPI provides relative URLs, make them absolute
|
|
return "https:" + icon
|
|
}
|