This commit is contained in:
Tomas Dvorak
2026-01-26 08:13:18 +01:00
parent aa036b6550
commit dfc079288f
505 changed files with 95755 additions and 5712 deletions
+125 -25
View File
@@ -515,17 +515,77 @@ func foldAccents(s string) string {
// Optional query: q= filters by home/away/venue/competition
func (bc *BaseController) GetMatchesHistory(c *gin.Context) {
p := filepath.Join("cache", "prefetch", "events_past.json")
f, err := os.Open(p)
if err != nil {
c.JSON(http.StatusNoContent, gin.H{"message": "No cached past matches"})
return
}
defer f.Close()
var matches []map[string]interface{}
if err := json.NewDecoder(f).Decode(&matches); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Cannot parse cached past matches"})
return
if f, err := os.Open(p); err == nil {
defer f.Close()
if err := json.NewDecoder(f).Decode(&matches); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Cannot parse cached past matches"})
return
}
} else {
p2 := filepath.Join("cache", "prefetch", "facr_club_info.json")
if f2, err2 := os.Open(p2); err2 == nil {
defer f2.Close()
var facr struct {
Competitions []struct {
Matches []struct {
MatchID string `json:"match_id"`
Home string `json:"home"`
Away string `json:"away"`
Venue string `json:"venue"`
DateTime string `json:"date_time"`
Score string `json:"score"`
HomeLogoURL string `json:"home_logo_url"`
AwayLogoURL string `json:"away_logo_url"`
HomeID string `json:"home_id"`
AwayID string `json:"away_id"`
HomeTeamID string `json:"home_team_id"`
AwayTeamID string `json:"away_team_id"`
} `json:"matches"`
} `json:"competitions"`
}
if err := json.NewDecoder(f2).Decode(&facr); err == nil {
now := time.Now()
for _, c := range facr.Competitions {
for _, m := range c.Matches {
dt := strings.TrimSpace(m.DateTime)
if dt == "" {
continue
}
ts, perr := time.ParseInLocation("02.01.2006 15:04", dt, time.Local)
if perr != nil || ts.After(now) {
continue
}
row := map[string]interface{}{
"match_id": m.MatchID,
"home": m.Home,
"away": m.Away,
"venue": m.Venue,
"date_time": m.DateTime,
"score": m.Score,
"home_logo_url": m.HomeLogoURL,
"away_logo_url": m.AwayLogoURL,
}
if m.HomeTeamID != "" {
row["home_team_id"] = m.HomeTeamID
} else if m.HomeID != "" {
row["home_team_id"] = m.HomeID
row["home_id"] = m.HomeID
}
if m.AwayTeamID != "" {
row["away_team_id"] = m.AwayTeamID
} else if m.AwayID != "" {
row["away_team_id"] = m.AwayID
row["away_id"] = m.AwayID
}
matches = append(matches, row)
}
}
}
} else {
c.JSON(http.StatusNoContent, gin.H{"message": "No cached past matches"})
return
}
}
// Apply overrides (same as in GetMatches)
@@ -922,7 +982,7 @@ func (bc *BaseController) GetZoneramaAlbum(c *gin.Context) {
return
}
req.Header.Set("User-Agent", "fotbal-club/zonerama-proxy")
client := &http.Client{Timeout: 25 * time.Second}
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
@@ -1907,7 +1967,6 @@ func (bc *BaseController) GetCompetitionAliases(c *gin.Context) {
c.JSON(http.StatusOK, items)
}
// Admin: create or replace alias by code (idempotent)
func (bc *BaseController) PutCompetitionAlias(c *gin.Context) {
code := strings.TrimSpace(c.Param("code"))
if code == "" {
@@ -1928,7 +1987,7 @@ func (bc *BaseController) PutCompetitionAlias(c *gin.Context) {
return
}
var item models.CompetitionAlias
if err := bc.DB.Where("code = ?", code).First(&item).Error; err != nil {
if err := bc.DB.Unscoped().Where("code = ?", code).First(&item).Error; err != nil {
if err == gorm.ErrRecordNotFound {
item = models.CompetitionAlias{Code: code}
} else {
@@ -1947,6 +2006,12 @@ func (bc *BaseController) PutCompetitionAlias(c *gin.Context) {
return
}
} else {
if item.DeletedAt.Valid {
if err := bc.DB.Unscoped().Model(&item).Update("deleted_at", nil).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Nelze obnovit alias"})
return
}
}
if err := bc.DB.Save(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Nelze uložit alias"})
return
@@ -1964,7 +2029,7 @@ func (bc *BaseController) DeleteCompetitionAlias(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Chyba code"})
return
}
if err := bc.DB.Where("code = ?", code).Delete(&models.CompetitionAlias{}).Error; err != nil {
if err := bc.DB.Unscoped().Where("code = ?", code).Delete(&models.CompetitionAlias{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Nelze smazat alias"})
return
}
@@ -3056,8 +3121,15 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
}
services.StartErrorReviewAutoRegister(bc.DB)
if strings.TrimSpace(s.ClubID) != "" {
if url, err := services.CacheClubLogo(bc.DB, strings.TrimSpace(s.ClubID)); err == nil && strings.TrimSpace(url) != "" {
_ = bc.DB.Model(&models.Settings{}).Where("id = ?", s.ID).Update("club_logo_url", url).Error
if clubInfo, err := services.CacheClubLogoAndName(bc.DB, strings.TrimSpace(s.ClubID)); err == nil {
if strings.TrimSpace(clubInfo.LogoURL) != "" {
_ = bc.DB.Model(&models.Settings{}).Where("id = ?", s.ID).Update("club_logo_url", clubInfo.LogoURL).Error
}
// Update club name if it's empty and we got one from logoapi
if strings.TrimSpace(s.ClubName) == "" && strings.TrimSpace(clubInfo.ClubName) != "" {
_ = bc.DB.Model(&models.Settings{}).Where("id = ?", s.ID).Update("club_name", clubInfo.ClubName).Error
s.ClubName = clubInfo.ClubName // Update local variable for logging
}
}
}
go func(snap models.Settings) {
@@ -3462,8 +3534,15 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
services.StartErrorReviewAutoRegister(bc.DB)
if strings.TrimSpace(s.ClubID) != "" {
go func(id uint, clubID string) {
if url, err := services.CacheClubLogo(bc.DB, strings.TrimSpace(clubID)); err == nil && strings.TrimSpace(url) != "" {
_ = bc.DB.Model(&models.Settings{}).Where("id = ?", id).Update("club_logo_url", url).Error
if clubInfo, err := services.CacheClubLogoAndName(bc.DB, strings.TrimSpace(clubID)); err == nil {
if strings.TrimSpace(clubInfo.LogoURL) != "" {
_ = bc.DB.Model(&models.Settings{}).Where("id = ?", id).Update("club_logo_url", clubInfo.LogoURL).Error
}
// Update club name if it's empty and we got one from logoapi
var currentSettings models.Settings
if bc.DB.First(&currentSettings, id).Error == nil && strings.TrimSpace(currentSettings.ClubName) == "" && strings.TrimSpace(clubInfo.ClubName) != "" {
_ = bc.DB.Model(&models.Settings{}).Where("id = ?", id).Update("club_name", clubInfo.ClubName).Error
}
}
}(s.ID, s.ClubID)
}
@@ -4130,8 +4209,15 @@ func (bc *BaseController) UpdateSettings(c *gin.Context) {
services.StartErrorReviewAutoRegister(bc.DB)
if strings.TrimSpace(s.ClubID) != "" && (strings.HasPrefix(strings.ToLower(strings.TrimSpace(s.ClubLogoURL)), "http://") || strings.HasPrefix(strings.ToLower(strings.TrimSpace(s.ClubLogoURL)), "https://") || strings.TrimSpace(s.ClubLogoURL) == "") {
go func(id uint, clubID string) {
if url, err := services.CacheClubLogo(bc.DB, strings.TrimSpace(clubID)); err == nil && strings.TrimSpace(url) != "" {
_ = bc.DB.Model(&models.Settings{}).Where("id = ?", id).Update("club_logo_url", url).Error
if clubInfo, err := services.CacheClubLogoAndName(bc.DB, strings.TrimSpace(clubID)); err == nil {
if strings.TrimSpace(clubInfo.LogoURL) != "" {
_ = bc.DB.Model(&models.Settings{}).Where("id = ?", id).Update("club_logo_url", clubInfo.LogoURL).Error
}
// Update club name if it's empty and we got one from logoapi
var currentSettings models.Settings
if bc.DB.First(&currentSettings, id).Error == nil && strings.TrimSpace(currentSettings.ClubName) == "" && strings.TrimSpace(clubInfo.ClubName) != "" {
_ = bc.DB.Model(&models.Settings{}).Where("id = ?", id).Update("club_name", clubInfo.ClubName).Error
}
}
}(s.ID, s.ClubID)
}
@@ -4264,7 +4350,9 @@ func (bc *BaseController) GetPublicSettings(c *gin.Context) {
"club_logo_url": s.ClubLogoURL,
"club_url": s.ClubURL,
// Runtime flags (env-based)
"premium": config.AppConfig.Premium,
"premium": config.AppConfig.Premium,
"club_data_mode": config.AppConfig.ClubDataMode,
"eshop_enabled": config.AppConfig.EshopEnabled,
// Theme
"primary_color": s.PrimaryColor,
@@ -4361,6 +4449,16 @@ func (bc *BaseController) GetSettings(c *gin.Context) {
}
s.LoadCustomNav()
s.LoadVideosOverrides()
// Decode manual videos for admin payload (populate transient exported fields)
if strings.TrimSpace(s.VideosJSON) != "" {
var vids []string
_ = json.Unmarshal([]byte(s.VideosJSON), &vids)
s.Videos = vids
}
if strings.TrimSpace(s.VideosItemsJSON) != "" {
// Unmarshal directly into the transient anonymous struct field type
_ = json.Unmarshal([]byte(s.VideosItemsJSON), &s.VideosItems)
}
// derive map form for admin consumers
mv := map[string]string{}
if len(s.VideosOverrides) > 0 {
@@ -5169,7 +5267,7 @@ func (bc *BaseController) UploadImage(c *gin.Context) {
}
name := strings.TrimSpace(f.Filename)
ext := strings.ToLower(filepath.Ext(name))
// Allow images, PDFs, Office docs, text, archives, and common media
// Allow images, PDFs, Office docs, text, archives, and common media (including webm audio)
allowed := map[string]bool{
// Images
".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, ".svg": true,
@@ -5178,7 +5276,7 @@ func (bc *BaseController) UploadImage(c *gin.Context) {
// Archives
".zip": true, ".rar": true, ".7z": true, ".tar": true, ".gz": true,
// Media
".mp4": true, ".avi": true, ".mov": true, ".mp3": true, ".wav": true,
".mp4": true, ".avi": true, ".mov": true, ".mp3": true, ".wav": true, ".webm": true,
}
if !allowed[ext] {
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported file type"})
@@ -5223,8 +5321,10 @@ func (bc *BaseController) UploadImage(c *gin.Context) {
validCT = strings.HasPrefix(dl, "text/") || dl == "application/octet-stream"
case ".mp4", ".avi", ".mov":
validCT = strings.HasPrefix(dl, "video/") || dl == "application/octet-stream"
case ".mp3", ".wav":
validCT = strings.HasPrefix(dl, "audio/") || dl == "application/octet-stream"
case ".mp3", ".wav", ".webm":
// Some browsers label MediaRecorder audio-only blobs as video/webm,
// so allow both audio/* and video/* for webm uploads in addition to a generic octet-stream.
validCT = strings.HasPrefix(dl, "audio/") || strings.HasPrefix(dl, "video/") || dl == "application/octet-stream"
default:
validCT = strings.HasPrefix(dl, "image/")
}