299 lines
7.6 KiB
Go
299 lines
7.6 KiB
Go
// backend/meta.go
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// --------------------------
|
|
// generated/meta/<id>/meta.json
|
|
// --------------------------
|
|
|
|
type previewClip struct {
|
|
StartSeconds float64 `json:"startSeconds"`
|
|
DurationSeconds float64 `json:"durationSeconds"`
|
|
}
|
|
|
|
type previewSpriteMeta struct {
|
|
Path string `json:"path"` // z.B. /api/preview-sprite/<id>
|
|
Count int `json:"count"` // Anzahl Frames im Sprite
|
|
Cols int `json:"cols"` // Spalten im Tile
|
|
Rows int `json:"rows"` // Zeilen im Tile
|
|
StepSeconds float64 `json:"stepSeconds"` // Zeitabstand zwischen Frames (z.B. 5)
|
|
CellWidth int `json:"cellWidth,omitempty"`
|
|
CellHeight int `json:"cellHeight,omitempty"`
|
|
}
|
|
|
|
type videoMeta struct {
|
|
Version int `json:"version"`
|
|
DurationSeconds float64 `json:"durationSeconds"`
|
|
FileSize int64 `json:"fileSize"`
|
|
FileModUnix int64 `json:"fileModUnix"`
|
|
|
|
VideoWidth int `json:"videoWidth,omitempty"`
|
|
VideoHeight int `json:"videoHeight,omitempty"`
|
|
FPS float64 `json:"fps,omitempty"`
|
|
Resolution string `json:"resolution,omitempty"` // z.B. "1920x1080"
|
|
|
|
SourceURL string `json:"sourceUrl,omitempty"`
|
|
PreviewClips []previewClip `json:"previewClips,omitempty"`
|
|
PreviewSprite *previewSpriteMeta `json:"previewSprite,omitempty"`
|
|
|
|
UpdatedAtUnix int64 `json:"updatedAtUnix"`
|
|
}
|
|
|
|
// liest Meta (v2 ODER altes v1) und validiert gegen fi (Size/ModTime)
|
|
func readVideoMeta(metaPath string, fi os.FileInfo) (dur float64, w int, h int, fps float64, ok bool) {
|
|
b, err := os.ReadFile(metaPath)
|
|
if err != nil || len(b) == 0 {
|
|
return 0, 0, 0, 0, false
|
|
}
|
|
|
|
// 1) Neues Format (oder v1 mit gleichen Feldern)
|
|
var m videoMeta
|
|
if err := json.Unmarshal(b, &m); err == nil && (m.Version == 2 || m.Version == 1) {
|
|
if m.FileSize != fi.Size() || m.FileModUnix != fi.ModTime().Unix() {
|
|
return 0, 0, 0, 0, false
|
|
}
|
|
if m.DurationSeconds <= 0 {
|
|
return 0, 0, 0, 0, false
|
|
}
|
|
return m.DurationSeconds, m.VideoWidth, m.VideoHeight, m.FPS, true
|
|
}
|
|
|
|
// 2) Fallback: ganz altes v1-Format (nur Duration etc.)
|
|
var m1 struct {
|
|
Version int `json:"version"`
|
|
DurationSeconds float64 `json:"durationSeconds"`
|
|
FileSize int64 `json:"fileSize"`
|
|
FileModUnix int64 `json:"fileModUnix"`
|
|
UpdatedAtUnix int64 `json:"updatedAtUnix"`
|
|
}
|
|
if err := json.Unmarshal(b, &m1); err != nil {
|
|
return 0, 0, 0, 0, false
|
|
}
|
|
if m1.Version != 1 {
|
|
return 0, 0, 0, 0, false
|
|
}
|
|
if m1.FileSize != fi.Size() || m1.FileModUnix != fi.ModTime().Unix() {
|
|
return 0, 0, 0, 0, false
|
|
}
|
|
if m1.DurationSeconds <= 0 {
|
|
return 0, 0, 0, 0, false
|
|
}
|
|
return m1.DurationSeconds, 0, 0, 0, true
|
|
}
|
|
|
|
func readVideoMetaDuration(metaPath string, fi os.FileInfo) (float64, bool) {
|
|
m, ok := readVideoMetaIfValid(metaPath, fi)
|
|
if !ok || m == nil || m.DurationSeconds <= 0 {
|
|
return 0, false
|
|
}
|
|
return m.DurationSeconds, true
|
|
}
|
|
|
|
func readVideoMetaSourceURL(metaPath string, fi os.FileInfo) (string, bool) {
|
|
m, ok := readVideoMetaIfValid(metaPath, fi)
|
|
if !ok || m == nil {
|
|
return "", false
|
|
}
|
|
u := strings.TrimSpace(m.SourceURL)
|
|
if u == "" {
|
|
return "", false
|
|
}
|
|
return u, true
|
|
}
|
|
|
|
// Voll-Write (wenn du dur + props schon hast)
|
|
func writeVideoMeta(metaPath string, fi os.FileInfo, dur float64, w int, h int, fps float64, sourceURL string) error {
|
|
if strings.TrimSpace(metaPath) == "" || dur <= 0 {
|
|
return nil
|
|
}
|
|
m := videoMeta{
|
|
Version: 2,
|
|
DurationSeconds: dur,
|
|
FileSize: fi.Size(),
|
|
FileModUnix: fi.ModTime().Unix(),
|
|
VideoWidth: w,
|
|
VideoHeight: h,
|
|
FPS: fps,
|
|
Resolution: formatResolution(w, h),
|
|
SourceURL: strings.TrimSpace(sourceURL),
|
|
UpdatedAtUnix: time.Now().Unix(),
|
|
}
|
|
buf, err := json.Marshal(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf = append(buf, '\n')
|
|
return atomicWriteFile(metaPath, buf)
|
|
}
|
|
|
|
func writeVideoMetaWithPreviewClips(metaPath string, fi os.FileInfo, dur float64, w int, h int, fps float64, sourceURL string, clips []previewClip) error {
|
|
if strings.TrimSpace(metaPath) == "" || dur <= 0 {
|
|
return nil
|
|
}
|
|
m := videoMeta{
|
|
Version: 2,
|
|
DurationSeconds: dur,
|
|
FileSize: fi.Size(),
|
|
FileModUnix: fi.ModTime().Unix(),
|
|
VideoWidth: w,
|
|
VideoHeight: h,
|
|
FPS: fps,
|
|
Resolution: formatResolution(w, h),
|
|
SourceURL: strings.TrimSpace(sourceURL),
|
|
PreviewClips: clips,
|
|
UpdatedAtUnix: time.Now().Unix(),
|
|
}
|
|
buf, err := json.Marshal(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf = append(buf, '\n')
|
|
return atomicWriteFile(metaPath, buf)
|
|
}
|
|
|
|
func writeVideoMetaWithPreviewClipsAndSprite(
|
|
metaPath string,
|
|
fi os.FileInfo,
|
|
dur float64,
|
|
w int,
|
|
h int,
|
|
fps float64,
|
|
sourceURL string,
|
|
clips []previewClip,
|
|
sprite *previewSpriteMeta,
|
|
) error {
|
|
if strings.TrimSpace(metaPath) == "" || dur <= 0 {
|
|
return nil
|
|
}
|
|
|
|
m := videoMeta{
|
|
Version: 2,
|
|
DurationSeconds: dur,
|
|
FileSize: fi.Size(),
|
|
FileModUnix: fi.ModTime().Unix(),
|
|
VideoWidth: w,
|
|
VideoHeight: h,
|
|
FPS: fps,
|
|
Resolution: formatResolution(w, h),
|
|
SourceURL: strings.TrimSpace(sourceURL),
|
|
PreviewClips: clips,
|
|
PreviewSprite: sprite,
|
|
UpdatedAtUnix: time.Now().Unix(),
|
|
}
|
|
|
|
buf, err := json.Marshal(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf = append(buf, '\n')
|
|
return atomicWriteFile(metaPath, buf)
|
|
}
|
|
|
|
// Duration-only Write (ohne props)
|
|
func writeVideoMetaDuration(metaPath string, fi os.FileInfo, dur float64, sourceURL string) error {
|
|
return writeVideoMeta(metaPath, fi, dur, 0, 0, 0, sourceURL)
|
|
}
|
|
|
|
func generatedMetaFile(assetID string) (string, error) {
|
|
assetID = stripHotPrefix(strings.TrimSpace(assetID))
|
|
if assetID == "" {
|
|
return "", fmt.Errorf("empty assetID")
|
|
}
|
|
|
|
// exakt wie beim Schreiben normalisieren
|
|
id, err := sanitizeID(assetID)
|
|
if err != nil || id == "" {
|
|
return "", fmt.Errorf("invalid assetID: %w", err)
|
|
}
|
|
|
|
return metaJSONPathForAssetID(id)
|
|
}
|
|
|
|
// ✅ Neu: /generated/meta/<id>/...
|
|
func generatedDirForID(id string) (string, error) {
|
|
id, err := sanitizeID(id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
root, err := generatedMetaRoot()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if strings.TrimSpace(root) == "" {
|
|
return "", fmt.Errorf("generated meta root ist leer")
|
|
}
|
|
return filepath.Join(root, id), nil
|
|
}
|
|
|
|
func ensureGeneratedDir(id string) (string, error) {
|
|
dir, err := generatedDirForID(id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
return dir, nil
|
|
}
|
|
|
|
func formatResolution(w, h int) string {
|
|
if w <= 0 || h <= 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%dx%d", w, h)
|
|
}
|
|
|
|
func generatedThumbFile(id string) (string, error) {
|
|
dir, err := generatedDirForID(id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(dir, "thumbs.webp"), nil
|
|
}
|
|
|
|
func generatedPreviewFile(id string) (string, error) {
|
|
dir, err := generatedDirForID(id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(dir, "preview.mp4"), nil
|
|
}
|
|
|
|
func generatedPreviewSpriteFile(id string) (string, error) {
|
|
dir, err := generatedDirForID(id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(dir, "preview-sprite.webp"), nil
|
|
}
|
|
|
|
func ensureGeneratedDirs() error {
|
|
root, err := generatedMetaRoot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(root) == "" {
|
|
return fmt.Errorf("generated meta root ist leer")
|
|
}
|
|
return os.MkdirAll(root, 0o755)
|
|
}
|
|
|
|
func sanitizeID(id string) (string, error) {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return "", fmt.Errorf("id fehlt")
|
|
}
|
|
if strings.ContainsAny(id, `/\`) {
|
|
return "", fmt.Errorf("ungültige id")
|
|
}
|
|
return id, nil
|
|
}
|