This commit is contained in:
Tomáš Dvořák
2025-10-16 13:32:05 +02:00
commit 12cba639b9
663 changed files with 168914 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
package controllers
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"fotbal-club/internal/services"
)
// PrefetchController exposes admin endpoints to inspect and trigger background prefetch cycles.
type PrefetchController struct{}
// isDuringMatchFromCache checks cached FACR JSONs to determine if a match is ongoing.
func isDuringMatchFromCache(cacheDir string) bool {
now := time.Now()
parseDT := func(s string) (time.Time, bool) {
s = strings.TrimSpace(s)
if s == "" { return time.Time{}, false }
layouts := []string{"02.01.2006 15:04", time.RFC3339, "2006-01-02 15:04"}
for _, layout := range layouts {
if t, err := time.ParseInLocation(layout, s, time.Local); err == nil { return t, true }
}
return time.Time{}, false
}
within := func(ts time.Time) bool {
start := ts.Add(-15 * time.Minute)
end := ts.Add(150 * time.Minute)
return now.After(start) && now.Before(end)
}
type flatMatch struct {
DateTime string `json:"date_time"`
Date string `json:"date"`
Time string `json:"time"`
Kickoff string `json:"kickoff"`
}
check := func(b []byte) bool {
if len(b) == 0 { return false }
var payload struct {
Competitions []struct { Matches []struct {
DateTime string `json:"date_time"`
Date string `json:"date"`
Time string `json:"time"`
Kickoff string `json:"kickoff"`
} `json:"matches"` } `json:"competitions"`
Matches []flatMatch `json:"matches"`
}
_ = json.Unmarshal(b, &payload)
for _, c := range payload.Competitions {
for _, m := range c.Matches {
var ts time.Time; var ok bool
if m.DateTime != "" { ts, ok = parseDT(m.DateTime) } else if m.Date != "" || m.Time != "" { ts, ok = parseDT(strings.TrimSpace(m.Date+" "+m.Time)) } else if m.Kickoff != "" { ts, ok = parseDT(m.Kickoff) }
if ok && within(ts) { return true }
}
}
for _, m := range payload.Matches {
var ts time.Time; var ok bool
if m.DateTime != "" { ts, ok = parseDT(m.DateTime) } else if m.Date != "" || m.Time != "" { ts, ok = parseDT(strings.TrimSpace(m.Date+" "+m.Time)) } else if m.Kickoff != "" { ts, ok = parseDT(m.Kickoff) }
if ok && within(ts) { return true }
}
return false
}
files := []string{ filepath.Join(cacheDir, "facr_club_info.json"), filepath.Join(cacheDir, "facr_tables.json") }
for _, p := range files {
if b, err := os.ReadFile(p); err == nil {
if check(b) { return true }
}
}
return false
}
func NewPrefetchController() *PrefetchController { return &PrefetchController{} }
// Status returns info about the last fetch and the approximate next run time.
// GET /api/v1/admin/prefetch/status
func (pc *PrefetchController) Status(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
cacheDir := filepath.Join("cache", "prefetch")
metaPath := filepath.Join(cacheDir, "meta.json")
var lastUpdated string
if b, err := os.ReadFile(metaPath); err == nil {
var m struct{ LastUpdated string `json:"lastUpdated"` }
if json.Unmarshal(b, &m) == nil {
lastUpdated = m.LastUpdated
}
}
// Determine configured interval
interval := 30 * time.Minute
if v := strings.TrimSpace(os.Getenv("PREFETCH_INTERVAL_MINUTES")); v != "" {
if mins, err := strconv.Atoi(v); err == nil && mins > 0 {
interval = time.Duration(mins) * time.Minute
}
}
// Fast mode if during match
fast := isDuringMatchFromCache(cacheDir)
next := time.Now().Add(interval)
if fast {
next = time.Now().Add(5 * time.Minute)
}
c.JSON(http.StatusOK, gin.H{
"lastUpdated": lastUpdated,
"intervalMinutes": int(interval / time.Minute),
"fastMode": fast,
"nextApproximate": next.Format(time.RFC3339),
})
}
// Trigger starts an immediate prefetch cycle (best effort)
// POST /api/v1/admin/prefetch/trigger
func (pc *PrefetchController) Trigger(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
// Resolve the base URL similar to main.go logic
base := os.Getenv("PREFETCH_TARGET")
if strings.TrimSpace(base) == "" {
port := strings.TrimSpace(os.Getenv("PORT"))
if port == "" { port = "8080" }
base = "http://127.0.0.1:" + port + "/api/v1"
}
go func() {
// fire-and-forget
services.PrefetchOnce(base)
// Additionally trigger an immediate Zonerama refresh based on cached settings
// This ensures the admin trigger updates the gallery right away.
cacheDir := filepath.Join("cache", "prefetch")
b, err := os.ReadFile(filepath.Join(cacheDir, "settings.json"))
if err == nil {
var s struct{
ZoneramaURL string `json:"zonerama_url"`
GalleryURL string `json:"gallery_url"`
}
if jsonErr := json.Unmarshal(b, &s); jsonErr == nil {
link := strings.TrimSpace(s.ZoneramaURL)
if link == "" { link = strings.TrimSpace(s.GalleryURL) }
if link != "" {
_ = services.RefreshZoneramaNow(link)
}
}
}
// Regenerate flat gallery files from existing albums
_ = services.RegenerateFlatGalleryFiles()
}()
c.JSON(http.StatusOK, gin.H{"message": "Prefetch started"})
}