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

414 lines
12 KiB
Go

package controllers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"fotbal-club/internal/services"
"fotbal-club/pkg/logger"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type GalleryController struct {
DB *gorm.DB
}
func NewGalleryController(db *gorm.DB) *GalleryController {
return &GalleryController{DB: db}
}
// ZoneramaAlbum represents a single album with photos
type ZoneramaAlbum struct {
ID string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
Date string `json:"date"`
PhotosCount int `json:"photos_count"`
ViewsCount int `json:"views_count"`
Photos []ZoneramaPhoto `json:"photos"`
FetchedAt string `json:"fetched_at,omitempty"`
}
// ZoneramaPhoto represents a single photo
type ZoneramaPhoto struct {
ID string `json:"id"`
PageURL string `json:"page_url"`
Image1500 string `json:"image_1500"`
}
// ZoneramaProfile represents the profile with list of albums (metadata only)
type ZoneramaProfile struct {
InputLink string `json:"input_link"`
Albums []struct {
ID string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
Date string `json:"date"`
PhotosCount int `json:"photos_count"`
ViewsCount int `json:"views_count"`
} `json:"albums"`
FetchedAt string `json:"fetched_at"`
}
// GetGalleryAlbums returns all fetched albums (public)
func (gc *GalleryController) GetGalleryAlbums(c *gin.Context) {
albumsFile := filepath.Join("cache", "prefetch", "zonerama_albums.json")
data, err := os.ReadFile(albumsFile)
if err != nil {
c.JSON(http.StatusNoContent, gin.H{"albums": []interface{}{}})
return
}
var albums []ZoneramaAlbum
if err := json.Unmarshal(data, &albums); err != nil {
logger.Error("Failed to parse albums JSON: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid albums data"})
return
}
c.JSON(http.StatusOK, gin.H{"albums": albums})
}
// GetGalleryAlbum returns a single album by ID (public)
func (gc *GalleryController) GetGalleryAlbum(c *gin.Context) {
albumID := c.Param("id")
if albumID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing album ID"})
return
}
albumsFile := filepath.Join("cache", "prefetch", "zonerama_albums.json")
data, err := os.ReadFile(albumsFile)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Albums not found"})
return
}
var albums []ZoneramaAlbum
if err := json.Unmarshal(data, &albums); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid albums data"})
return
}
for _, album := range albums {
if album.ID == albumID {
c.JSON(http.StatusOK, album)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "Album not found"})
}
// GetGalleryProfile returns the profile metadata (admin)
func (gc *GalleryController) GetGalleryProfile(c *gin.Context) {
profileFile := filepath.Join("cache", "prefetch", "zonerama_profile.json")
data, err := os.ReadFile(profileFile)
if err != nil {
c.JSON(http.StatusNoContent, gin.H{})
return
}
var profile ZoneramaProfile
if err := json.Unmarshal(data, &profile); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid profile data"})
return
}
c.JSON(http.StatusOK, profile)
}
// FetchAlbum fetches a single album and adds it to the albums collection (admin)
func (gc *GalleryController) FetchAlbum(c *gin.Context) {
var body struct {
Link string `json:"link" binding:"required"`
PhotoLimit int `json:"photo_limit"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
return
}
// Validate it's an album URL
if !strings.Contains(body.Link, "/Album/") {
c.JSON(http.StatusBadRequest, gin.H{"error": "Link must be a Zonerama album URL (must contain /Album/)"})
return
}
// Set default photo limit
if body.PhotoLimit == 0 {
body.PhotoLimit = 50 // Default to 50 photos per album
}
// Call external API
apiURL := fmt.Sprintf("https://zonerama.tdvorak.dev/zonerama-album?link=%s&photo_limit=%d",
body.Link, body.PhotoLimit)
logger.Info("Fetching album from Zonerama API: %s", apiURL)
client := &http.Client{Timeout: 60 * time.Second}
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"})
return
}
req.Header.Set("User-Agent", "fotbal-club/1.0")
resp, err := client.Do(req)
if err != nil {
logger.Error("Album fetch failed: %v", err)
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to fetch album: " + err.Error()})
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
logger.Error("Album API returned status %d", resp.StatusCode)
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Zonerama API returned status %d", resp.StatusCode)})
return
}
// Zonerama API returns {"input_link": "...", "albums": [...]}
var apiResponse struct {
InputLink string `json:"input_link"`
Albums []ZoneramaAlbum `json:"albums"`
}
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
logger.Error("Failed to parse album response: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse album data"})
return
}
if len(apiResponse.Albums) == 0 {
logger.Error("No albums returned from API")
c.JSON(http.StatusNotFound, gin.H{"error": "Album not found in API response"})
return
}
// Get the first album (should be the only one for single album endpoint)
albumData := apiResponse.Albums[0]
// Add fetched timestamp
albumData.FetchedAt = time.Now().Format(time.RFC3339)
// Load existing albums
albumsFile := filepath.Join("cache", "prefetch", "zonerama_albums.json")
var albums []ZoneramaAlbum
if data, err := os.ReadFile(albumsFile); err == nil {
_ = json.Unmarshal(data, &albums)
}
// Check if album already exists and update it, or add new
found := false
for i, a := range albums {
if a.ID == albumData.ID {
albums[i] = albumData
found = true
logger.Info("Updated existing album: %s", albumData.ID)
break
}
}
if !found {
albums = append([]ZoneramaAlbum{albumData}, albums...) // Prepend new album
logger.Info("Added new album: %s", albumData.ID)
}
// Save back to file
if err := os.MkdirAll(filepath.Dir(albumsFile), 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create cache directory"})
return
}
albumsJSON, err := json.MarshalIndent(albums, "", " ")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize albums"})
return
}
if err := os.WriteFile(albumsFile, albumsJSON, 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save albums"})
return
}
logger.Info("Album %s saved successfully with %d photos", albumData.ID, len(albumData.Photos))
// Regenerate flat gallery files for frontend consumption
if err := services.RegenerateFlatGalleryFiles(); err != nil {
logger.Error("Failed to regenerate flat gallery files: %v", err)
// Don't fail the request, just log the error
}
c.JSON(http.StatusOK, gin.H{
"message": "Album fetched and saved successfully",
"album": albumData,
})
}
// DeleteAlbum removes an album from the collection (admin)
func (gc *GalleryController) DeleteAlbum(c *gin.Context) {
albumID := c.Param("id")
if albumID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing album ID"})
return
}
albumsFile := filepath.Join("cache", "prefetch", "zonerama_albums.json")
data, err := os.ReadFile(albumsFile)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Albums not found"})
return
}
var albums []ZoneramaAlbum
if err := json.Unmarshal(data, &albums); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid albums data"})
return
}
// Find and remove
newAlbums := []ZoneramaAlbum{}
found := false
for _, album := range albums {
if album.ID != albumID {
newAlbums = append(newAlbums, album)
} else {
found = true
}
}
if !found {
c.JSON(http.StatusNotFound, gin.H{"error": "Album not found"})
return
}
// Save back
albumsJSON, _ := json.MarshalIndent(newAlbums, "", " ")
if err := os.WriteFile(albumsFile, albumsJSON, 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save albums"})
return
}
logger.Info("Deleted album: %s", albumID)
// Regenerate flat gallery files for frontend consumption
if err := services.RegenerateFlatGalleryFiles(); err != nil {
logger.Error("Failed to regenerate flat gallery files: %v", err)
// Don't fail the request, just log the error
}
c.JSON(http.StatusOK, gin.H{"message": "Album deleted successfully"})
}
// RefreshFromZonerama triggers a refresh of the Zonerama gallery from the configured URL (admin)
func (gc *GalleryController) RefreshFromZonerama(c *gin.Context) {
// Load settings to get the Zonerama URL
var settings struct {
GalleryURL string `json:"gallery_url"`
}
if err := gc.DB.Table("settings").Select("gallery_url").First(&settings).Error; err != nil {
logger.Error("Failed to load settings: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load gallery settings"})
return
}
zoneramaURL := strings.TrimSpace(settings.GalleryURL)
if zoneramaURL == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Zonerama URL is not configured in settings"})
return
}
// Validate it's a Zonerama URL
if !strings.Contains(strings.ToLower(zoneramaURL), "zonerama.com") {
c.JSON(http.StatusBadRequest, gin.H{"error": "Configured gallery URL is not a Zonerama URL"})
return
}
logger.Info("Triggering Zonerama refresh from: %s", zoneramaURL)
// Call the refresh service in a goroutine to avoid blocking
go func() {
if err := services.RefreshZoneramaNow(zoneramaURL); err != nil {
logger.Error("Zonerama refresh failed: %v", err)
} else {
logger.Info("Zonerama refresh completed successfully")
// Regenerate flat gallery files for frontend consumption
if err := services.RegenerateFlatGalleryFiles(); err != nil {
logger.Error("Failed to regenerate flat gallery files: %v", err)
}
}
}()
c.JSON(http.StatusOK, gin.H{
"message": "Zonerama refresh started",
"url": zoneramaURL,
})
}
// ProxyImage proxies image requests to Zonerama to avoid CORS issues (public)
func (gc *GalleryController) ProxyImage(c *gin.Context) {
imageURL := c.Query("url")
if imageURL == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing image URL"})
return
}
// Validate that the URL is from Zonerama
if !strings.Contains(imageURL, "zonerama.com") {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image URL"})
return
}
// Fetch the image
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequest("GET", imageURL, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"})
return
}
req.Header.Set("User-Agent", "fotbal-club/1.0")
resp, err := client.Do(req)
if err != nil {
logger.Error("Image fetch failed: %v", err)
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to fetch image"})
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
logger.Error("Image fetch returned status %d", resp.StatusCode)
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to fetch image"})
return
}
// Copy headers
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
}
}
// Set CORS headers
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET")
// Stream the image
c.Status(resp.StatusCode)
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
logger.Error("Failed to stream image: %v", err)
}
}