Add gallery admin and video media support.

This updates gallery handling to support video playback with generated poster thumbnails, adds authenticated admin upload/delete flows, and improves dev/runtime behavior including reliable thumbnail generation and media-safe response handling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-02 23:01:02 +12:00
parent 509e7ccb43
commit 45b31be9a7
22 changed files with 1002 additions and 217 deletions

View File

@@ -1,10 +1,13 @@
package gallery
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
@@ -29,7 +32,8 @@ func EnsureThumbnails(imagesDir string) error {
return err
}
return filepath.WalkDir(imagesDir, func(path string, d fs.DirEntry, err error) error {
var failures []string
walkErr := filepath.WalkDir(imagesDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
@@ -39,7 +43,7 @@ func EnsureThumbnails(imagesDir string) error {
}
return nil
}
if !isJPEG(d.Name()) {
if !isGalleryMedia(d.Name()) {
return nil
}
@@ -52,19 +56,43 @@ func EnsureThumbnails(imagesDir string) error {
return nil
}
if isVideo(d.Name()) {
thumbDst := filepath.Join(imagesDir, thumbDirName, filepath.FromSlash(videoPosterRel(rel)))
if err := os.MkdirAll(filepath.Dir(thumbDst), 0o755); err != nil {
failures = append(failures, fmt.Sprintf("%s: %v", rel, err))
return nil
}
if err := writeVideoPoster(path, thumbDst, gridThumbMaxEdge); err != nil {
failures = append(failures, fmt.Sprintf("%s: %v", rel, err))
}
return nil
}
thumbDst := filepath.Join(imagesDir, thumbDirName, filepath.FromSlash(rel))
heroDst := filepath.Join(imagesDir, heroDirName, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(thumbDst), 0o755); err != nil {
return err
failures = append(failures, fmt.Sprintf("%s: %v", rel, err))
return nil
}
if err := os.MkdirAll(filepath.Dir(heroDst), 0o755); err != nil {
return err
failures = append(failures, fmt.Sprintf("%s: %v", rel, err))
return nil
}
if err := writeThumb(path, thumbDst, gridThumbMaxEdge, gridThumbQuality); err != nil {
return err
failures = append(failures, fmt.Sprintf("%s thumb: %v", rel, err))
}
return writeThumb(path, heroDst, heroThumbMaxEdge, heroThumbQuality)
if err := writeThumb(path, heroDst, heroThumbMaxEdge, heroThumbQuality); err != nil {
failures = append(failures, fmt.Sprintf("%s hero: %v", rel, err))
}
return nil
})
if walkErr != nil {
return walkErr
}
if len(failures) > 0 {
return fmt.Errorf("%d file(s): %s", len(failures), strings.Join(failures, "; "))
}
return nil
}
// EnsurePriority generates thumbnails for one image before first page load.
@@ -87,6 +115,41 @@ func EnsurePriority(imagesDir, relPath string) error {
return writeThumb(src, thumbDst, gridThumbMaxEdge, gridThumbQuality)
}
func writeVideoPoster(src, dst string, maxEdge int) error {
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
if dstInfo, err := os.Stat(dst); err == nil && !dstInfo.ModTime().Before(srcInfo.ModTime()) {
return nil
}
if _, err := exec.LookPath("ffmpeg"); err != nil {
return nil
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
scale := fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease", maxEdge, maxEdge)
cmd := exec.Command("ffmpeg",
"-hide_banner", "-loglevel", "error", "-y",
"-i", src,
"-vf", scale,
"-frames:v", "1",
"-q:v", "5",
dst,
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
msg := strings.TrimSpace(stderr.String())
if msg == "" {
return err
}
return fmt.Errorf("%w: %s", err, msg)
}
return nil
}
func writeThumb(src, dst string, maxEdge, quality int) error {
srcInfo, err := os.Stat(src)
if err != nil {