mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-05 03:02:56 +00:00
hot fix #1
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user