package main import ( "bytes" "database/sql" "fmt" "log" "net/http" neturl "net/url" "os" "path/filepath" "strconv" "strings" "time" "unicode" "github.com/PuerkitoBio/goquery" "github.com/gin-gonic/gin" "github.com/google/uuid" "golang.org/x/text/unicode/norm" ) // ==================== Club Handlers ==================== func searchClubs(c *gin.Context) { q := strings.TrimSpace(c.Query("q")) if q == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"}) return } clubs, err := scrapeFotbalSearch(q) if err != nil || len(clubs) == 0 { nq := removeDiacritics(strings.ToLower(q)) if nq != strings.ToLower(q) { if c2, err2 := scrapeFotbalSearch(nq); err2 == nil && len(c2) > 0 { c.JSON(http.StatusOK, c2) return } } c.JSON(http.StatusOK, getDemoClubs(q)) return } c.JSON(http.StatusOK, clubs) } func getClub(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "club ID is required"}) return } club, err := fetchClubByID(id) if err != nil || club == nil { c.JSON(http.StatusNotFound, gin.H{"error": "club not found"}) return } c.JSON(http.StatusOK, club) } type ClubSearchWithLogoResult struct { ID string `json:"id"` Name string `json:"name"` LogoURL string `json:"logo_url,omitempty"` HasLocalLogo bool `json:"has_local_logo"` } func searchClubsWithLogos(c *gin.Context) { q := strings.TrimSpace(c.Query("q")) if q == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"}) return } sport := strings.ToLower(strings.TrimSpace(c.DefaultQuery("sport", c.DefaultQuery("type", "")))) base := "SELECT id, club_name, has_svg, has_png FROM logos" where := "" args := []interface{}{} if q != "" { where = " WHERE (LOWER(club_name) LIKE ? OR id LIKE ?)" like := "%" + strings.ToLower(q) + "%" args = append(args, like, "%"+q+"%") } if sport != "" && sport != "all" { if where == "" { where = " WHERE " } else { where += " AND " } where += "LOWER(club_type) = ?" args = append(args, sport) } query := base + where + " ORDER BY club_name" rows, err := db.Query(query, args...) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) return } defer rows.Close() scheme := "http" if c.Request.TLS != nil { scheme = "https" } baseURL := fmt.Sprintf("%s://%s", scheme, c.Request.Host) results := []ClubSearchWithLogoResult{} for rows.Next() { var id, name string var hasSVG, hasPNG int if err := rows.Scan(&id, &name, &hasSVG, &hasPNG); err != nil { continue } logoURL := "" if hasPNG == 1 { logoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, id) } else if hasSVG == 1 { logoURL = fmt.Sprintf("%s/logos/%s?format=svg", baseURL, id) } res := ClubSearchWithLogoResult{ ID: id, Name: name, LogoURL: logoURL, HasLocalLogo: hasSVG == 1 || hasPNG == 1, } results = append(results, res) } if err := rows.Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) return } c.JSON(http.StatusOK, results) } func scrapeFotbalSearch(q string) ([]Club, error) { vals := neturl.Values{} vals.Set("q", q) searchURL := "https://www.fotbal.cz/club/hledej?" + vals.Encode() req, _ := http.NewRequest("GET", searchURL, nil) req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8") req.Header.Set("Accept-Language", "cs-CZ,cs;q=0.9,en;q=0.8") client := &http.Client{Timeout: 12 * time.Second} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { vals2 := neturl.Values{} vals2.Set("q", "\""+q+"\"") searchURL = "https://www.fotbal.cz/club/hledej?" + vals2.Encode() req2, _ := http.NewRequest("GET", searchURL, nil) req2.Header = req.Header.Clone() resp2, err2 := client.Do(req2) if err2 != nil { return nil, err2 } defer resp2.Body.Close() if resp2.StatusCode != http.StatusOK { return []Club{}, nil } resp = resp2 } buf := new(bytes.Buffer) _, _ = buf.ReadFrom(resp.Body) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(buf.Bytes())) if err != nil { return nil, err } clubs := []Club{} doc.Find("li.ListItemSplit").Each(func(_ int, li *goquery.Selection) { a := li.Find("a.Link--inverted").First() href := strings.TrimSpace(a.AttrOr("href", "")) if href == "" { return } name := strings.TrimSpace(a.Find("span.H7").First().Text()) if name == "" { name = strings.TrimSpace(a.Text()) } logoURL := strings.TrimSpace(a.Find("img").First().AttrOr("src", "")) address := strings.TrimSpace(li.Find(".ClubAddress p").First().Text()) clubType := "football" if strings.Contains(strings.ToLower(href), "/futsal/") { clubType = "futsal" } parts := strings.Split(strings.TrimRight(href, "/"), "/") clubID := "" if len(parts) > 0 { clubID = parts[len(parts)-1] } if !strings.HasPrefix(href, "http://") && !strings.HasPrefix(href, "https://") { href = "https://www.fotbal.cz" + href } city := extractCityFromAddress(address) clubs = append(clubs, Club{ID: clubID, Name: name, City: city, Type: clubType, Website: href, LogoURL: logoURL}) }) return clubs, nil } func fetchClubByID(id string) (*Club, error) { tryFetch := func(base string, typ string) (*Club, error) { url := fmt.Sprintf("%s/%s", base, id) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") req.Header.Set("Accept-Language", "cs-CZ,cs;q=0.9,en;q=0.8") client := &http.Client{Timeout: 12 * time.Second} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("status %d", resp.StatusCode) } doc, err := goquery.NewDocumentFromReader(resp.Body) if err != nil { return nil, err } name := strings.TrimSpace(doc.Find("h1.H4 span").First().Text()) address := strings.TrimSpace(doc.Find(".ClubAddress p").First().Text()) city := extractCityFromAddress(address) logo := fmt.Sprintf("https://is1.fotbal.cz/media/kluby/%s/%s_crop.jpg", id, id) return &Club{ID: id, Name: name, City: city, Type: typ, Website: "", LogoURL: logo}, nil } if club, err := tryFetch("https://www.fotbal.cz/souteze/club/club", "football"); err == nil && club != nil && club.Name != "" { return club, nil } if club, err := tryFetch("https://www.fotbal.cz/futsal/club/club", "futsal"); err == nil && club != nil && club.Name != "" { return club, nil } return nil, fmt.Errorf("not found") } func removeDiacritics(s string) string { d := norm.NFD.String(s) b := make([]rune, 0, len(d)) for _, r := range d { if unicode.Is(unicode.Mn, r) { continue } b = append(b, r) } return string(b) } // Demo data fallback func getDemoClubs(query string) []Club { demoClubs := []Club{ { ID: "11111111-2222-3333-4444-555555555555", Name: "SK Slavia Praha", City: "Praha", Type: "football", Website: "https://www.slavia.cz", }, { ID: "22222222-3333-4444-5555-666666666666", Name: "AC Sparta Praha", City: "Praha", Type: "football", Website: "https://www.sparta.cz", }, { ID: "33333333-4444-5555-6666-777777777777", Name: "FC Viktoria Plzeň", City: "Plzeň", Type: "football", Website: "https://www.fcviktoria.cz", }, { ID: "44444444-5555-6666-7777-888888888888", Name: "FC Baník Ostrava", City: "Ostrava", Type: "football", Website: "https://www.fcb.cz", }, { ID: "55555555-6666-7777-8888-999999999999", Name: "SK Sigma Olomouc", City: "Olomouc", Type: "football", Website: "https://www.sigmafotbal.cz", }, { ID: "66666666-7777-8888-9999-aaaaaaaaaaaa", Name: "FC Slovan Liberec", City: "Liberec", Type: "football", Website: "https://www.fcslovanliberec.cz", }, { ID: "77777777-8888-9999-aaaa-bbbbbbbbbbbb", Name: "MFK Karviná", City: "Karviná", Type: "football", Website: "https://www.mfkkarvina.cz", }, { ID: "88888888-9999-aaaa-bbbb-cccccccccccc", Name: "FC Fastav Zlín", City: "Zlín", Type: "football", Website: "https://www.fczlin.cz", }, { ID: "99999999-aaaa-bbbb-cccc-dddddddddddd", Name: "FK Jablonec", City: "Jablonec nad Nisou", Type: "football", Website: "https://www.fkjablonec.cz", }, { ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Name: "SFC Opava", City: "Opava", Type: "football", Website: "https://www.sfcopava.cz", }, { ID: "bbbbbbbb-cccc-dddd-eeee-ffffffffffff", Name: "FK Teplice", City: "Teplice", Type: "football", Website: "https://www.fkteplice.cz", }, { ID: "cccccccc-dddd-eeee-ffff-000000000000", Name: "1. FK Příbram", City: "Příbram", Type: "football", Website: "https://www.1fkpribram.cz", }, { ID: "dddddddd-eeee-ffff-0000-111111111111", Name: "SK Dynamo České Budějovice", City: "České Budějovice", Type: "football", Website: "https://www.dynamocb.cz", }, { ID: "eeeeeeee-ffff-0000-1111-222222222222", Name: "FC Zbrojovka Brno", City: "Brno", Type: "football", Website: "https://www.fczbrno.cz", }, { ID: "ffffffff-0000-1111-2222-333333333333", Name: "FC Vysočina Jihlava", City: "Jihlava", Type: "football", Website: "https://www.fcvysocina.cz", }, { ID: "00000000-1111-2222-3333-444444444444", Name: "FK Mladá Boleslav", City: "Mladá Boleslav", Type: "football", Website: "https://www.fkmb.cz", }, { ID: "10101010-1111-2222-3333-444444444444", Name: "SK Sigma Hranice", City: "Hranice", Type: "football", Website: "", }, { ID: "20202020-2222-3333-4444-555555555555", Name: "SK Hranice", City: "Hranice", Type: "football", Website: "", }, { ID: "30303030-3333-4444-5555-666666666666", Name: "TJ Krnov", City: "Krnov", Type: "football", Website: "", }, } var results []Club lowerQuery := strings.ToLower(query) // Fuzzy matching: check contains in name, city, and partial matches for _, club := range demoClubs { lowerName := strings.ToLower(club.Name) lowerCity := strings.ToLower(club.City) // Exact contains match in name or city if strings.Contains(lowerName, lowerQuery) || strings.Contains(lowerCity, lowerQuery) { results = append(results, club) continue } // Fuzzy match: check if query matches start of any word in name words := strings.Fields(lowerName) for _, word := range words { if strings.HasPrefix(word, lowerQuery) { results = append(results, club) break } } } return results } // ==================== Logo Handlers ==================== type LogoMetadata struct { ID string `json:"id"` ClubName string `json:"club_name"` ClubCity string `json:"club_city,omitempty"` ClubType string `json:"club_type,omitempty"` ClubWebsite string `json:"club_website,omitempty"` HasSVG bool `json:"has_svg"` HasPNG bool `json:"has_png"` PrimaryFormat string `json:"primary_format"` LogoURL string `json:"logo_url"` LogoURLSVG string `json:"logo_url_svg,omitempty"` LogoURLPNG string `json:"logo_url_png,omitempty"` FileSizeSVG int64 `json:"file_size_svg,omitempty"` FileSizePNG int64 `json:"file_size_png,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // getLogo returns the logo file (PNG preferred, SVG fallback) func getLogo(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "logo ID is required"}) return } // Validate UUID format if _, err := uuid.Parse(id); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid UUID format"}) return } // Check format preference from query format := c.Query("format") // can be "svg" or "png" var logoPath string var contentType string var found bool // Try PNG first (primary format) if format == "" || format == "png" { pngPath := filepath.Join("./logos/png", id+".png") if _, err := os.Stat(pngPath); err == nil { logoPath = pngPath contentType = "image/png" found = true } } // Try SVG if PNG not found or explicitly requested if !found && (format == "" || format == "svg") { svgPath := filepath.Join("./logos/svg", id+".svg") if _, err := os.Stat(svgPath); err == nil { logoPath = svgPath contentType = "image/svg+xml" found = true } } if !found { c.JSON(http.StatusNotFound, gin.H{"error": "logo not found"}) return } // Set CORS headers explicitly for file serving c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, OPTIONS") c.Header("Access-Control-Allow-Headers", "*") c.Header("Content-Type", contentType) c.Header("Cache-Control", "public, max-age=31536000") c.File(logoPath) } func getLogoWithMetadata(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "logo ID is required"}) return } // Validate UUID format if _, err := uuid.Parse(id); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid UUID format"}) return } // Get metadata from database var metadata LogoMetadata var hasSVG, hasPNG int err := db.QueryRow(` SELECT id, club_name, club_city, club_type, club_website, has_svg, has_png, primary_format, file_size_svg, file_size_png, created_at, updated_at FROM logos WHERE id = ? `, id).Scan( &metadata.ID, &metadata.ClubName, &metadata.ClubCity, &metadata.ClubType, &metadata.ClubWebsite, &hasSVG, &hasPNG, &metadata.PrimaryFormat, &metadata.FileSizeSVG, &metadata.FileSizePNG, &metadata.CreatedAt, &metadata.UpdatedAt, ) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "logo not found"}) return } if err != nil { log.Printf("Database error: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) return } metadata.HasSVG = hasSVG == 1 metadata.HasPNG = hasPNG == 1 // Construct logo URLs scheme := "http" if c.Request.TLS != nil { scheme = "https" } baseURL := fmt.Sprintf("%s://%s", scheme, c.Request.Host) // Primary URL (PNG preferred) if metadata.HasPNG { metadata.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, id) } else if metadata.HasSVG { metadata.LogoURL = fmt.Sprintf("%s/logos/%s?format=svg", baseURL, id) } // Format-specific URLs if metadata.HasSVG { metadata.LogoURLSVG = fmt.Sprintf("%s/logos/%s?format=svg", baseURL, id) } if metadata.HasPNG { metadata.LogoURLPNG = fmt.Sprintf("%s/logos/%s?format=png", baseURL, id) } c.JSON(http.StatusOK, metadata) } // List all logos func listLogos(c *gin.Context) { q := strings.TrimSpace(c.Query("q")) sport := strings.ToLower(strings.TrimSpace(c.DefaultQuery("sport", c.DefaultQuery("type", "")))) sortParam := c.DefaultQuery("sort", "name") limitStr := c.Query("limit") pageStr := c.Query("page") base := "SELECT id, club_name, club_city, club_type, club_website, has_svg, has_png, primary_format, created_at, updated_at FROM logos" where := "" args := []interface{}{} if q != "" { where = " WHERE LOWER(club_name) LIKE ? OR LOWER(club_city) LIKE ? OR id LIKE ?" like := "%" + strings.ToLower(q) + "%" args = append(args, like, like, "%"+q+"%") } if sport != "" && sport != "all" { if where == "" { where = " WHERE LOWER(club_type) = ?" } else { where += " AND LOWER(club_type) = ?" } args = append(args, sport) } order := " ORDER BY club_name" if sortParam == "recent" { order = " ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC" } limitClause := "" if limitStr != "" { if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { limitClause = " LIMIT ?" args = append(args, limit) if pageStr != "" { if page, err := strconv.Atoi(pageStr); err == nil { if page < 1 { page = 1 } offset := (page - 1) * limit limitClause += " OFFSET ?" args = append(args, offset) } } } } query := base + where + order + limitClause rows, err := db.Query(query, args...) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) return } defer rows.Close() var logos []LogoMetadata scheme := "http" if c.Request.TLS != nil { scheme = "https" } baseURL := fmt.Sprintf("%s://%s", scheme, c.Request.Host) for rows.Next() { var logo LogoMetadata var hasSVG, hasPNG int if err := rows.Scan( &logo.ID, &logo.ClubName, &logo.ClubCity, &logo.ClubType, &logo.ClubWebsite, &hasSVG, &hasPNG, &logo.PrimaryFormat, &logo.CreatedAt, &logo.UpdatedAt, ); err != nil { continue } logo.HasSVG = hasSVG == 1 logo.HasPNG = hasPNG == 1 if logo.HasPNG { logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, logo.ID) } else if logo.HasSVG { logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=svg", baseURL, logo.ID) } logos = append(logos, logo) } if q != "" && len(logos) == 0 { limitClause2 := "" args2 := []interface{}{} if limitStr != "" { if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { limitClause2 = " LIMIT ?" args2 = append(args2, limit) if pageStr != "" { if page, err := strconv.Atoi(pageStr); err == nil { if page < 1 { page = 1 } offset := (page - 1) * limit limitClause2 += " OFFSET ?" args2 = append(args2, offset) } } } } q2 := base + order + limitClause2 rows2, err2 := db.Query(q2, args2...) if err2 == nil { defer rows2.Close() normQ := removeDiacritics(strings.ToLower(q)) tmp := []LogoMetadata{} for rows2.Next() { var logo LogoMetadata var hasSVG2, hasPNG2 int if err := rows2.Scan( &logo.ID, &logo.ClubName, &logo.ClubCity, &logo.ClubType, &logo.ClubWebsite, &hasSVG2, &hasPNG2, &logo.PrimaryFormat, &logo.CreatedAt, &logo.UpdatedAt, ); err != nil { continue } logo.HasSVG = hasSVG2 == 1 logo.HasPNG = hasPNG2 == 1 if logo.HasPNG { logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=png", baseURL, logo.ID) } else if logo.HasSVG { logo.LogoURL = fmt.Sprintf("%s/logos/%s?format=svg", baseURL, logo.ID) } nameN := removeDiacritics(strings.ToLower(logo.ClubName)) cityN := removeDiacritics(strings.ToLower(logo.ClubCity)) if strings.Contains(nameN, normQ) || strings.Contains(cityN, normQ) || strings.Contains(strings.ToLower(logo.ID), strings.ToLower(q)) { tmp = append(tmp, logo) } } logos = tmp } } c.JSON(http.StatusOK, logos) } func deleteLogo(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "logo ID is required"}) return } if _, err := uuid.Parse(id); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid UUID format"}) return } _, err := db.Exec("DELETE FROM logos WHERE id = ?", id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) return } pngPath := filepath.Join("./logos/png", id+".png") svgPath := filepath.Join("./logos/svg", id+".svg") os.Remove(pngPath) os.Remove(svgPath) c.JSON(http.StatusOK, gin.H{"success": true, "id": id}) } func uploadLogo(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "logo ID is required"}) return } // Validate UUID format if _, err := uuid.Parse(id); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid UUID format"}) return } // Read metadata from form clubName := c.PostForm("club_name") clubCity := c.PostForm("club_city") clubType := c.PostForm("club_type") clubWebsite := c.PostForm("club_website") if clubName == "" { if club, err := fetchClubByID(id); err == nil && club != nil { if club.Name != "" { clubName = club.Name } if clubType == "" && club.Type != "" { clubType = club.Type } if clubCity == "" && club.City != "" { clubCity = club.City } if clubWebsite == "" && club.Website != "" { clubWebsite = club.Website } } if clubName == "" { clubName = "Club " + id } } // Get uploaded file file, err := c.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "no file provided"}) return } ext := strings.ToLower(filepath.Ext(file.Filename)) if ext != ".svg" && ext != ".png" && ext != ".pdf" { c.JSON(http.StatusBadRequest, gin.H{"error": "only .svg, .png and .pdf files are allowed"}) return } // Determine storage paths var svgPath, pngPath string var hasSVG, hasPNG int var sizeSVG, sizePNG int64 if ext == ".svg" || ext == ".pdf" { pngPath = filepath.Join("./logos/png", id+".png") if ext == ".svg" { svgPath = filepath.Join("./logos/svg", id+".svg") // Save SVG if err := c.SaveUploadedFile(file, svgPath); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save SVG file"}) return } // Get SVG file size if stat, err := os.Stat(svgPath); err == nil { sizeSVG = stat.Size() } hasSVG = 1 // Convert SVG to PNG log.Printf("Converting SVG to PNG for club: %s", clubName) if err := ConvertSVGToPNG(svgPath, pngPath, 512); err != nil { log.Printf("Warning: Failed to convert SVG to PNG: %v", err) // Don't fail the upload, just log the warning } else { // Optimize PNG if err := OptimizePNG(pngPath); err != nil { log.Printf("Warning: Failed to optimize PNG: %v", err) } // Get PNG file size if stat, err := os.Stat(pngPath); err == nil { sizePNG = stat.Size() hasPNG = 1 } } } else { // PDF file - convert directly to PNG pdfTempPath := filepath.Join("./logos/temp", id+".pdf") os.MkdirAll("./logos/temp", 0755) if err := c.SaveUploadedFile(file, pdfTempPath); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save PDF file"}) return } log.Printf("Converting PDF to PNG for club: %s", clubName) if err := ConvertPDFToPNG(pdfTempPath, pngPath, 512); err != nil { log.Printf("Error: Failed to convert PDF to PNG: %v", err) os.Remove(pdfTempPath) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to convert PDF to PNG"}) return } // Clean up temp PDF os.Remove(pdfTempPath) // Optimize PNG if err := OptimizePNG(pngPath); err != nil { log.Printf("Warning: Failed to optimize PNG: %v", err) } // Get PNG file size if stat, err := os.Stat(pngPath); err == nil { sizePNG = stat.Size() hasPNG = 1 } } } else { // PNG upload pngPath = filepath.Join("./logos/png", id+".png") if err := c.SaveUploadedFile(file, pngPath); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save PNG file"}) return } // Optimize PNG if err := OptimizePNG(pngPath); err != nil { log.Printf("Warning: Failed to optimize PNG: %v", err) } // Get PNG file size if stat, err := os.Stat(pngPath); err == nil { sizePNG = stat.Size() } hasPNG = 1 } // Save metadata to database _, err = db.Exec(` INSERT OR REPLACE INTO logos ( id, club_name, club_city, club_type, club_website, has_svg, has_png, primary_format, file_size_svg, file_size_png, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, 'png', ?, ?, CURRENT_TIMESTAMP) `, id, clubName, clubCity, clubType, clubWebsite, hasSVG, hasPNG, sizeSVG, sizePNG) if err != nil { log.Printf("Database error: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save metadata"}) return } response := gin.H{ "success": true, "id": id, "club_name": clubName, "has_svg": hasSVG == 1, "has_png": hasPNG == 1, "size_svg": sizeSVG, "size_png": sizePNG, "message": "logo uploaded successfully", } c.JSON(http.StatusOK, response) }