mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user