231 lines
5.1 KiB
Go
231 lines
5.1 KiB
Go
// backend\assets_generate.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func formatBytesSI(b int64) string {
|
|
if b < 0 {
|
|
b = 0
|
|
}
|
|
const unit = 1024
|
|
if b < unit {
|
|
return fmt.Sprintf("%d B", b)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := b / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
suffix := []string{"KB", "MB", "GB", "TB", "PB"}
|
|
v := float64(b) / float64(div)
|
|
// 1 Nachkommastelle, außer sehr große ganze Zahlen
|
|
if v >= 10 {
|
|
return fmt.Sprintf("%.0f %s", v, suffix[exp])
|
|
}
|
|
return fmt.Sprintf("%.1f %s", v, suffix[exp])
|
|
}
|
|
|
|
func u64ToI64(x uint64) int64 {
|
|
if x > uint64(maxInt64) {
|
|
return maxInt64
|
|
}
|
|
return int64(x)
|
|
}
|
|
|
|
func ensureAssetsForVideo(videoPath string) error {
|
|
// Default: keine SourceURL (für Covers egal)
|
|
return ensureAssetsForVideoWithProgress(videoPath, "", nil)
|
|
}
|
|
|
|
// Optional: für Stellen, wo du die URL hast (z.B. Postwork / Jobs)
|
|
func ensureAssetsForVideoWithSource(videoPath string, sourceURL string) error {
|
|
return ensureAssetsForVideoWithProgress(videoPath, sourceURL, nil)
|
|
}
|
|
|
|
// onRatio: 0..1 (Assets-Gesamtfortschritt)
|
|
func ensureAssetsForVideoWithProgress(videoPath string, sourceURL string, onRatio func(r float64)) error {
|
|
videoPath = strings.TrimSpace(videoPath)
|
|
if videoPath == "" {
|
|
return nil
|
|
}
|
|
|
|
fi, statErr := os.Stat(videoPath)
|
|
if statErr != nil || fi.IsDir() || fi.Size() <= 0 {
|
|
return nil
|
|
}
|
|
|
|
// ✅ ID = Dateiname ohne Endung (immer OHNE "HOT " Prefix)
|
|
base := filepath.Base(videoPath)
|
|
id := strings.TrimSuffix(base, filepath.Ext(base))
|
|
id = stripHotPrefix(id)
|
|
if strings.TrimSpace(id) == "" {
|
|
return nil
|
|
}
|
|
|
|
assetDir, gerr := ensureGeneratedDir(id)
|
|
if gerr != nil || strings.TrimSpace(assetDir) == "" {
|
|
return fmt.Errorf("generated dir: %v", gerr)
|
|
}
|
|
|
|
metaPath := filepath.Join(assetDir, "meta.json")
|
|
|
|
// ---- Meta / Duration + Props (Width/Height/FPS/Resolution) ----
|
|
durSec := 0.0
|
|
vw, vh := 0, 0
|
|
fps := 0.0
|
|
|
|
// 1) Try cache (Meta) first (inkl. w/h/fps)
|
|
if d, mw, mh, mfps, ok := readVideoMeta(metaPath, fi); ok {
|
|
durSec, vw, vh, fps = d, mw, mh, mfps
|
|
} else {
|
|
// 2) Duration berechnen
|
|
dctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
|
|
d, derr := durationSecondsCached(dctx, videoPath)
|
|
cancel()
|
|
|
|
if derr == nil && d > 0 {
|
|
durSec = d
|
|
}
|
|
}
|
|
|
|
// 3) Wenn wir Duration haben, aber Props fehlen -> ffprobe holen und Voll-Meta schreiben
|
|
// (damit resolution wirklich in meta.json landet)
|
|
if durSec > 0 && (vw <= 0 || vh <= 0 || fps <= 0) {
|
|
pctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
|
|
// optional: durSem für ffprobe begrenzen (du hast es global)
|
|
if durSem != nil {
|
|
if err := durSem.Acquire(pctx); err == nil {
|
|
vw, vh, fps, _ = probeVideoProps(pctx, videoPath)
|
|
durSem.Release()
|
|
} else {
|
|
// wenn Acquire fehlschlägt, best-effort ohne props
|
|
}
|
|
} else {
|
|
vw, vh, fps, _ = probeVideoProps(pctx, videoPath)
|
|
}
|
|
}
|
|
|
|
// 4) Meta schreiben/aktualisieren:
|
|
// - schreibt resolution (über formatResolution) nur wenn vw/vh > 0
|
|
// - schreibt sourceURL wenn vorhanden
|
|
if durSec > 0 {
|
|
_ = writeVideoMeta(metaPath, fi, durSec, vw, vh, fps, sourceURL)
|
|
}
|
|
|
|
// Gewichte: thumbs klein, preview groß
|
|
const (
|
|
thumbsW = 0.25
|
|
previewW = 0.75
|
|
)
|
|
|
|
progress := func(r float64) {
|
|
if onRatio == nil {
|
|
return
|
|
}
|
|
if r < 0 {
|
|
r = 0
|
|
}
|
|
if r > 1 {
|
|
r = 1
|
|
}
|
|
onRatio(r)
|
|
}
|
|
|
|
progress(0)
|
|
|
|
// ----------------
|
|
// Thumbs
|
|
// ----------------
|
|
thumbPath := filepath.Join(assetDir, "thumbs.jpg")
|
|
if tfi, err := os.Stat(thumbPath); err == nil && !tfi.IsDir() && tfi.Size() > 0 {
|
|
progress(thumbsW)
|
|
} else {
|
|
progress(0.05)
|
|
|
|
genCtx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
|
defer cancel()
|
|
|
|
if err := thumbSem.Acquire(genCtx); err != nil {
|
|
// best-effort
|
|
progress(thumbsW)
|
|
goto PREVIEW
|
|
}
|
|
defer thumbSem.Release()
|
|
|
|
progress(0.10)
|
|
|
|
t := 0.0
|
|
if durSec > 0 {
|
|
t = durSec * 0.5
|
|
}
|
|
|
|
progress(0.15)
|
|
|
|
img, e1 := extractFrameAtTimeJPEG(videoPath, t)
|
|
if e1 != nil || len(img) == 0 {
|
|
img, e1 = extractLastFrameJPEG(videoPath)
|
|
if e1 != nil || len(img) == 0 {
|
|
img, e1 = extractFirstFrameJPEG(videoPath)
|
|
}
|
|
}
|
|
|
|
progress(0.20)
|
|
|
|
if e1 == nil && len(img) > 0 {
|
|
if err := atomicWriteFile(thumbPath, img); err != nil {
|
|
fmt.Println("⚠️ thumb write:", err)
|
|
}
|
|
}
|
|
|
|
progress(thumbsW)
|
|
}
|
|
|
|
PREVIEW:
|
|
// ----------------
|
|
// Preview
|
|
// ----------------
|
|
previewPath := filepath.Join(assetDir, "preview.mp4")
|
|
if pfi, err := os.Stat(previewPath); err == nil && !pfi.IsDir() && pfi.Size() > 0 {
|
|
progress(1)
|
|
return nil
|
|
}
|
|
|
|
genCtx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
|
defer cancel()
|
|
|
|
progress(thumbsW + 0.02)
|
|
|
|
if err := genSem.Acquire(genCtx); err != nil {
|
|
progress(1)
|
|
return nil
|
|
}
|
|
defer genSem.Release()
|
|
|
|
progress(thumbsW + 0.05)
|
|
|
|
if err := generateTeaserClipsMP4WithProgress(genCtx, videoPath, previewPath, 1.0, 18, func(r float64) {
|
|
if r < 0 {
|
|
r = 0
|
|
}
|
|
if r > 1 {
|
|
r = 1
|
|
}
|
|
progress(thumbsW + r*previewW)
|
|
}); err != nil {
|
|
fmt.Println("⚠️ preview clips:", err)
|
|
}
|
|
|
|
progress(1)
|
|
return nil
|
|
}
|