Files
MyClub/internal/services/facr_service.go
T
Tomáš Dvořák 12cba639b9 upload
2025-10-16 13:32:05 +02:00

221 lines
5.7 KiB
Go

package services
import (
"encoding/json"
"errors"
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"math"
"net/http"
"strings"
"time"
"fotbal-club/internal/models"
"github.com/muesli/clusters"
"github.com/muesli/kmeans"
)
type FACRService struct {
baseURL string
httpClient *http.Client
}
type FACRClubSearchResponse struct {
Query string `json:"query"`
Count int `json:"count"`
Results []struct {
Name string `json:"name"`
ClubID string `json:"club_id"`
ClubType string `json:"club_type"`
LogoURL string `json:"logo_url"`
Category string `json:"category"`
} `json:"results"`
}
// NewFACRService creates a new FACR service instance
func NewFACRService(baseURL string) *FACRService {
return &FACRService{
baseURL: strings.TrimSuffix(baseURL, "/"),
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// SearchClubs searches for clubs by name
func (s *FACRService) SearchClubs(query string) ([]models.ClubSearchResult, error) {
if query == "" {
return nil, errors.New("search query cannot be empty")
}
url := fmt.Sprintf("%s/club/search?q=%s", s.baseURL, query)
resp, err := s.httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("error making request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var result FACRClubSearchResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("error decoding response: %v", err)
}
var clubs []models.ClubSearchResult
for _, club := range result.Results {
clubs = append(clubs, models.ClubSearchResult{
ID: club.ClubID,
Name: club.Name,
Type: club.ClubType,
LogoURL: club.LogoURL,
Category: club.Category,
})
}
return clubs, nil
}
// extractColorsFromLogo extracts dominant colors from a logo image
func (s *FACRService) extractColorsFromLogo(logoURL string) (string, string, string, error) {
resp, err := s.httpClient.Get(logoURL)
if err != nil {
return "", "", "", fmt.Errorf("error downloading logo: %v", err)
}
defer resp.Body.Close()
img, _, err := image.Decode(resp.Body)
if err != nil {
return "", "", "", fmt.Errorf("error decoding image: %v", err)
}
bounds := img.Bounds()
var points []clusters.Observation
// Sample pixels (every 10th pixel for performance)
for y := bounds.Min.Y; y < bounds.Max.Y; y += 10 {
for x := bounds.Min.X; x < bounds.Max.X; x += 10 {
r, g, b, a := img.At(x, y).RGBA()
if a > 0x7FFF { // Skip transparent pixels
// Convert to float64 slice and create a new observation
point := []float64{
float64(r >> 8),
float64(g >> 8),
float64(b >> 8),
}
points = append(points, clusters.Coordinates(point))
}
}
}
if len(points) == 0 {
return "#1E40AF", "#F59E0B", "#1F2937", nil
}
// Find 3 dominant colors using k-means
km := kmeans.New()
clusters, err := km.Partition(points, 3) // Find 3 dominant colors
if err != nil {
return "", "", "", fmt.Errorf("error in k-means clustering: %v", err)
}
// Sort clusters by size (number of points in each cluster)
for i := 0; i < len(clusters); i++ {
for j := i + 1; j < len(clusters); j++ {
if len(clusters[j].Observations) > len(clusters[i].Observations) {
clusters[i], clusters[j] = clusters[j], clusters[i]
}
}
}
// Get primary and secondary colors
primary := clusters[0].Center
secondary := clusters[1%len(clusters)].Center
// Calculate text color based on brightness
brightness := (0.299*primary[0] + 0.587*primary[1] + 0.114*primary[2]) / 255.0
textColor := "#000000"
if brightness < 0.5 {
textColor = "#FFFFFF"
}
return rgbToHex(primary), rgbToHex(secondary), textColor, nil
}
// rgbToHex converts RGB values to hex color
func rgbToHex(rgb []float64) string {
return fmt.Sprintf("#%02X%02X%02X",
uint8(math.Round(rgb[0])),
uint8(math.Round(rgb[1])),
uint8(math.Round(rgb[2])),
)
}
// GetClubDetails fetches detailed information about a club
func (s *FACRService) GetClubDetails(clubID string) (*models.ClubInfo, error) {
if clubID == "" {
return nil, errors.New("club ID cannot be empty")
}
url := fmt.Sprintf("%s/club/football/%s", s.baseURL, clubID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("error creating request: %v", err)
}
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error making request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var club struct {
Name string `json:"name"`
ClubID string `json:"club_id"`
LogoURL string `json:"logo_url"`
Category string `json:"category"`
}
if err := json.NewDecoder(resp.Body).Decode(&club); err != nil {
return nil, fmt.Errorf("error decoding response: %v", err)
}
// Extract colors from logo if available
var primaryColor, secondaryColor, textColor string
var errExtract error
if club.LogoURL != "" {
primaryColor, secondaryColor, textColor, errExtract = s.extractColorsFromLogo(club.LogoURL)
if errExtract != nil {
// Log the error but continue with default colors
fmt.Printf("Warning: Could not extract colors from logo: %v\n", errExtract)
}
}
// Set default colors if extraction failed or wasn't attempted
if primaryColor == "" {
primaryColor = "#1E40AF" // Blue
secondaryColor = "#F59E0B" // Amber
textColor = "#1F2937" // Gray-800
}
return &models.ClubInfo{
FACRClubID: club.ClubID,
Name: club.Name,
ShortName: club.Name, // Using name as fallback for short name
LogoURL: club.LogoURL,
PrimaryColor: primaryColor,
SecondaryColor: secondaryColor,
TextColor: textColor,
}, nil
}