nsfwapp/backend/assets_generate.go
2026-02-09 12:29:19 +01:00

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
}