Initial commit: Lookbook personal collection app

Pinterest-like app for saving images, videos, quotes, and embeds.

Features:
- Go backend with PostgreSQL, SSR templates
- Console-based admin auth (login/logout via browser console)
- Item types: images, videos (ffmpeg transcoding), quotes, embeds
- Media stored as BLOBs in PostgreSQL
- OpenGraph metadata extraction for links
- Embed detection for YouTube, Vimeo, Twitter/X
- Masonry grid layout, item detail pages
- Tag system with filtering
- Refresh metadata endpoint with change warnings
- Replace media endpoint for updating item images/videos
This commit is contained in:
soup 2026-01-17 01:09:23 -05:00
commit cdcc5b5293
Signed by: soup
SSH key fingerprint: SHA256:GYxje8eQkJ6HZKzVWDdyOUF1TyDiprruGhE0Ym8qYDY
45 changed files with 4634 additions and 0 deletions

View file

@ -0,0 +1,50 @@
package handlers
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"lookbook/internal/data/media"
)
// HandleGetMedia handles GET /media/{id}
func HandleGetMedia(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
idStr := r.PathValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return nil
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
m, err := media.QFindByID(ctx, rc.DB, id)
if err != nil {
return err
}
if m == nil {
http.NotFound(w, r)
return nil
}
// Set caching headers (media is immutable)
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
w.Header().Set("Content-Type", m.ContentType)
w.Header().Set("Content-Length", strconv.Itoa(len(m.Data)))
// Add ETag for conditional requests
etag := fmt.Sprintf(`"%d"`, m.ID)
w.Header().Set("ETag", etag)
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return nil
}
w.Write(m.Data)
return nil
}