208 lines
4.4 KiB
Go
208 lines
4.4 KiB
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 ----
|
|
durSec := 0.0
|
|
if d, ok := readVideoMetaDuration(metaPath, fi); ok {
|
|
durSec = d
|
|
} else {
|
|
dctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
|
|
d, derr := durationSecondsCached(dctx, videoPath)
|
|
cancel()
|
|
|
|
if derr == nil && d > 0 {
|
|
durSec = d
|
|
// ✅ Duration-only meta schreiben (inkl. sourceURL)
|
|
_ = writeVideoMetaDuration(metaPath, fi, durSec, sourceURL)
|
|
}
|
|
}
|
|
|
|
// ✅ Wenn Duration aus Meta kam, aber SourceURL jetzt neu vorhanden ist,
|
|
// dann Meta "anreichern" (ohne ffprobe).
|
|
if durSec > 0 && strings.TrimSpace(sourceURL) != "" {
|
|
if u, ok := readVideoMetaSourceURL(metaPath, fi); !ok || strings.TrimSpace(u) == "" {
|
|
_ = writeVideoMetaDuration(metaPath, fi, durSec, 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
|
|
}
|