package services import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" "time" "fotbal-club/internal/config" ) // EnsureFACRLogosProcessed triggers post-processing of FACR logos referenced // in cached prefetch JSON files (facr_club_info.json, facr_tables.json). // It is idempotent and safe to call multiple times. func EnsureFACRLogosProcessed(cacheDir string) error { if config.AppConfig != nil && !config.AppConfig.RembgEnabled { return nil } var firstErr error clubInfo := filepath.Join(cacheDir, "facr_club_info.json") if _, err := os.Stat(clubInfo); err == nil { if err := postProcessFACRClubInfoLogos(cacheDir); err != nil && firstErr == nil { firstErr = fmt.Errorf("club_info: %w", err) } } tables := filepath.Join(cacheDir, "facr_tables.json") if _, err := os.Stat(tables); err == nil { if err := postProcessFACRTablesLogos(cacheDir); err != nil && firstErr == nil { firstErr = fmt.Errorf("tables: %w", err) } } return firstErr } // progress state for rembg batch var rembgMu sync.RWMutex var rembgRunning bool var rembgTotal int var rembgDone int var rembgStarted time.Time var rembgFinished *time.Time // RembgStatus represents current background removal progress type RembgStatus struct { Running bool `json:"running"` Total int `json:"total"` Done int `json:"done"` StartedAt time.Time `json:"started_at"` FinishedAt *time.Time `json:"finished_at,omitempty"` } // GetRembgStatus returns a snapshot of current rembg batch progress func GetRembgStatus() RembgStatus { rembgMu.RLock() defer rembgMu.RUnlock() return RembgStatus{ Running: rembgRunning, Total: rembgTotal, Done: rembgDone, StartedAt: rembgStarted, FinishedAt: rembgFinished, } } // StartFACRLogosBatch scans cached FACR JSONs and processes all referenced FACR logos // using ProcessFACRLogo. Progress is tracked via GetRembgStatus. If a batch is already // running, this call is a no-op and returns false. Otherwise returns true and starts // a background goroutine. func StartFACRLogosBatch(cacheDir string) bool { if config.AppConfig != nil && !config.AppConfig.RembgEnabled { return false } rembgMu.Lock() if rembgRunning { rembgMu.Unlock() return false } // Collect unique FACR logo URLs to determine total urls := collectFACRLogos(cacheDir) rembgRunning = true rembgTotal = len(urls) rembgDone = 0 rembgStarted = time.Now() rembgFinished = nil rembgMu.Unlock() if len(urls) == 0 { // nothing to do; mark finished quickly rembgMu.Lock() rembgRunning = false now := time.Now() rembgFinished = &now rembgMu.Unlock() return true } go func(urlList []string) { defer func() { // Best-effort: after processing, ensure JSONs are rewritten to point to processed URLs _ = EnsureFACRLogosProcessed(cacheDir) rembgMu.Lock() rembgRunning = false now := time.Now() rembgFinished = &now rembgMu.Unlock() }() for _, u := range urlList { _, _ = ProcessFACRLogo(u) // best effort; skip errors rembgMu.Lock() rembgDone++ rembgMu.Unlock() } }(urls) return true } // collectFACRLogos returns unique FACR-hosted logo URLs from cached prefetch JSONs. func collectFACRLogos(cacheDir string) []string { uniq := map[string]struct{}{} add := func(s string) { s = strings.TrimSpace(s) if s == "" { return } // Only FACR sources; skip already-processed local /uploads and other hosts ls := strings.ToLower(s) if strings.HasPrefix(ls, "/uploads/") || strings.HasPrefix(ls, "/dist/") { return } if !strings.Contains(ls, "fotbal.cz") { return } uniq[s] = struct{}{} } // facr_club_info.json if b, err := os.ReadFile(filepath.Join(cacheDir, "facr_club_info.json")); err == nil { var payload struct { Competitions []struct { Matches []struct { HomeLogoURL string `json:"home_logo_url"` AwayLogoURL string `json:"away_logo_url"` } `json:"matches"` } `json:"competitions"` } if json.Unmarshal(b, &payload) == nil { for _, c := range payload.Competitions { for _, m := range c.Matches { add(m.HomeLogoURL) add(m.AwayLogoURL) } } } } // facr_tables.json if b, err := os.ReadFile(filepath.Join(cacheDir, "facr_tables.json")); err == nil { var payload struct { Competitions []struct { Table struct { Overall []struct { TeamLogoURL string `json:"team_logo_url"` } `json:"overall"` } `json:"table"` } `json:"competitions"` } if json.Unmarshal(b, &payload) == nil { for _, c := range payload.Competitions { for _, r := range c.Table.Overall { add(r.TeamLogoURL) } } } } out := make([]string, 0, len(uniq)) for k := range uniq { out = append(out, k) } return out }