// 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 }