Add video as a media kind in photos: streaming upload with per-kind limits, ffprobe metadata, poster frame reusing the image variant pipeline, and a playback object (remux when web-compatible, else h264/yuv420p/aac transcode). Video jobs run on a separate pool with a long lease. Frontend gains a video viewer, grid badge/duration and video upload.
126 lines
3.8 KiB
Go
126 lines
3.8 KiB
Go
package photos
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"galo/internal/models"
|
|
"galo/internal/storage"
|
|
)
|
|
|
|
// fakeBackend satisfies storage.Backend by embedding the interface; only the
|
|
// methods exercised by the code under test are implemented.
|
|
type fakeBackend struct {
|
|
storage.Backend
|
|
}
|
|
|
|
func (fakeBackend) URL(_ context.Context, key string) (string, error) {
|
|
return "signed:" + key, nil
|
|
}
|
|
|
|
func (fakeBackend) Open(_ context.Context, _ string) (io.ReadCloser, error) {
|
|
return io.NopCloser(bytes.NewReader([]byte("fake-video-bytes"))), nil
|
|
}
|
|
|
|
func (fakeBackend) Put(context.Context, string, io.Reader, int64, string) error { return nil }
|
|
|
|
func TestAttachURLsPlayback(t *testing.T) {
|
|
svc := &Service{storage: fakeBackend{}}
|
|
ctx := context.Background()
|
|
|
|
p := &models.Photo{
|
|
StorageKey: "objects/a/original.mp4",
|
|
ThumbKey: "objects/a/thumb.webp",
|
|
PreviewKey: "objects/a/preview.webp",
|
|
PlaybackKey: "objects/a/playback.mp4",
|
|
}
|
|
if err := svc.attachURLs(ctx, p); err != nil {
|
|
t.Fatalf("attachURLs: %v", err)
|
|
}
|
|
if p.PlaybackURL != "signed:objects/a/playback.mp4" {
|
|
t.Fatalf("playback url = %q", p.PlaybackURL)
|
|
}
|
|
if p.ThumbURL != "signed:objects/a/thumb.webp" {
|
|
t.Fatalf("thumb url = %q", p.ThumbURL)
|
|
}
|
|
|
|
// An image without a playback binding exposes no playback URL.
|
|
img := &models.Photo{StorageKey: "objects/b/original.jpg", ThumbKey: "objects/b/thumb.webp"}
|
|
if err := svc.attachURLs(ctx, img); err != nil {
|
|
t.Fatalf("attachURLs image: %v", err)
|
|
}
|
|
if img.PlaybackURL != "" {
|
|
t.Fatalf("image playback url = %q, want empty", img.PlaybackURL)
|
|
}
|
|
}
|
|
|
|
func TestMediaKind(t *testing.T) {
|
|
if got := mediaKind(""); got != KindImage {
|
|
t.Fatalf("empty kind = %q, want image", got)
|
|
}
|
|
if got := mediaKind(KindVideo); got != KindVideo {
|
|
t.Fatalf("video kind = %q", got)
|
|
}
|
|
if got := mediaKind("bogus"); got != KindImage {
|
|
t.Fatalf("bogus kind = %q, want image", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveMedia(t *testing.T) {
|
|
pngHead := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}
|
|
cases := []struct {
|
|
name string
|
|
declared string
|
|
filename string
|
|
head []byte
|
|
wantKind string
|
|
wantMimePref string
|
|
}{
|
|
{"image header", "image/png", "x.png", nil, KindImage, "image/png"},
|
|
{"video header", "video/mp4", "clip.mp4", nil, KindVideo, "video/mp4"},
|
|
{"video by extension", "", "clip.mp4", nil, KindVideo, "video/mp4"},
|
|
{"image by extension", "", "x.jpg", nil, KindImage, "image/jpeg"},
|
|
{"sniff png", "", "", pngHead, KindImage, "image/png"},
|
|
{"generic mime by extension", "application/octet-stream", "movie.mkv", nil, KindVideo, ""},
|
|
{"unsupported", "text/plain", "notes.txt", nil, "", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
kind, mime := resolveMedia(tc.declared, tc.filename, tc.head)
|
|
if kind != tc.wantKind {
|
|
t.Fatalf("kind = %q, want %q", kind, tc.wantKind)
|
|
}
|
|
if tc.wantMimePref != "" && mime != tc.wantMimePref {
|
|
t.Fatalf("mime = %q, want %q", mime, tc.wantMimePref)
|
|
}
|
|
if kind != "" && !strings.HasPrefix(mime, kind+"/") {
|
|
t.Fatalf("mime %q does not match kind %q", mime, kind)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUploadRejectsUnsupportedAndDisabledVideo(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := &Service{albums: stubAlbums{}, imageMax: 1 << 20, videoMax: 1 << 30, videoEnabled: false}
|
|
|
|
_, err := svc.Upload(ctx, UploadInput{
|
|
AlbumID: "a", UserID: "u", Filename: "notes.txt", MimeType: "text/plain",
|
|
Reader: bytes.NewReader([]byte("hello")),
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "image or video") {
|
|
t.Fatalf("unsupported err = %v, want invalid_file", err)
|
|
}
|
|
|
|
_, err = svc.Upload(ctx, UploadInput{
|
|
AlbumID: "a", UserID: "u", Filename: "clip.mp4", MimeType: "video/mp4",
|
|
Reader: bytes.NewReader([]byte("....")),
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "not available") {
|
|
t.Fatalf("disabled video err = %v, want video_disabled", err)
|
|
}
|
|
}
|