mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
414 lines
12 KiB
Go
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)
|
|
}
|
|
}
|