From dffe5dde16d9e0ab2fdb41e91c28d69e36f1b0e3 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:46:38 +0100 Subject: [PATCH] updated to jpg --- backend/analyze.go | 8 +- backend/assets_generate.go | 22 +- backend/assets_sprite.go | 36 +-- backend/frontend.go | 2 - backend/live.go | 8 +- backend/main.go | 40 ++-- backend/meta.go | 4 +- backend/nsfw_detector.go | 4 - backend/preview.go | 208 ++++++++--------- backend/record.go | 2 +- backend/recorder.go | 49 ++-- backend/recorder_settings.json | 2 +- backend/settings.go | 4 +- frontend/src/components/ui/CategoriesTab.tsx | 2 +- frontend/src/components/ui/CookieModal.tsx | 212 +++++++++++++----- frontend/src/components/ui/Downloads.tsx | 14 +- .../ui/FinishedDownloadsGalleryView.tsx | 4 +- .../ui/FinishedDownloadsTableView.tsx | 6 +- frontend/src/components/ui/ModelDetails.tsx | 48 ++-- frontend/src/components/ui/ModelPreview.tsx | 10 +- frontend/src/components/ui/ModelsTab.tsx | 24 +- frontend/src/components/ui/Pagination.tsx | 2 +- frontend/src/components/ui/Player.tsx | 2 +- .../src/components/ui/RecorderSettings.tsx | 31 +-- frontend/src/components/ui/Tabs.tsx | 2 +- frontend/src/components/ui/TagBadge.tsx | 21 +- frontend/src/components/ui/TagOverflowRow.tsx | 74 ++++-- .../src/components/ui/VideoSplitModal.tsx | 4 +- 28 files changed, 480 insertions(+), 365 deletions(-) diff --git a/backend/analyze.go b/backend/analyze.go index 086d872..7ea4985 100644 --- a/backend/analyze.go +++ b/backend/analyze.go @@ -19,8 +19,6 @@ import ( "sort" "strings" "time" - - "golang.org/x/image/webp" ) type analyzeVideoReq struct { @@ -92,7 +90,7 @@ func extractSpriteFrames(spritePath string, ps previewSpriteMetaFileInfo) ([]ima } defer f.Close() - img, err := webp.Decode(f) + img, _, err := image.Decode(f) if err != nil { return nil, err } @@ -395,9 +393,9 @@ func analyzeVideoFromSprite(ctx context.Context, outPath, goal string) ([]analyz return nil, fmt.Errorf("previewSprite count fehlt") } - spritePath := filepath.Join(filepath.Dir(metaPath), "preview-sprite.webp") + spritePath := filepath.Join(filepath.Dir(metaPath), "preview-sprite.jpg") if fi, err := os.Stat(spritePath); err != nil || fi == nil || fi.IsDir() || fi.Size() <= 0 { - return nil, fmt.Errorf("preview-sprite.webp nicht gefunden") + return nil, fmt.Errorf("preview-sprite.jpg nicht gefunden") } durationSec, _ := durationSecondsForAnalyze(ctx, outPath) diff --git a/backend/assets_generate.go b/backend/assets_generate.go index 6f38e2f..d839942 100644 --- a/backend/assets_generate.go +++ b/backend/assets_generate.go @@ -70,7 +70,7 @@ func assetIDFromVideoPath(videoPath string) string { return strings.TrimSpace(id) } -// Liefert die standardisierten Pfade (preview.webp / preview.mp4 / preview-sprite.webp / meta.json) +// Liefert die standardisierten Pfade (preview.jpg / preview.mp4 / preview-sprite.jpg / meta.json) func assetPathsForID(id string) (assetDir, thumbPath, previewPath, spritePath, metaPath string, err error) { id = strings.TrimSpace(id) if id == "" { @@ -82,9 +82,9 @@ func assetPathsForID(id string) (assetDir, thumbPath, previewPath, spritePath, m return "", "", "", "", "", fmt.Errorf("generated dir: %v", err) } - thumbPath = filepath.Join(assetDir, "preview.webp") + thumbPath = filepath.Join(assetDir, "preview.jpg") previewPath = filepath.Join(assetDir, "preview.mp4") - spritePath = filepath.Join(assetDir, "preview-sprite.webp") + spritePath = filepath.Join(assetDir, "preview-sprite.jpg") metaPath, _ = metaJSONPathForAssetID(id) // bevorzugt generated/meta//meta.json if strings.TrimSpace(metaPath) == "" { @@ -294,7 +294,7 @@ func ensureAssetsForVideoDetailed(ctx context.Context, videoPath string, sourceU progress(0) // ---------------- - // Thumbs (WebP-only) + // Thumbs (JPG-only) // ---------------- if thumbBefore { progress(thumbsW) @@ -318,7 +318,7 @@ func ensureAssetsForVideoDetailed(ctx context.Context, videoPath string, sourceU progress(0.10) // ✅ Immer letztes Frame bevorzugen (Preview soll “Endzustand” zeigen) - img, e1 := extractLastFrameWebP(videoPath) + img, e1 := extractLastFrameJPG(videoPath) if e1 != nil || len(img) == 0 { // Fallback: wenn wir Duration kennen, versuche kurz vor Ende if meta.durSec > 0 { @@ -326,11 +326,11 @@ func ensureAssetsForVideoDetailed(ctx context.Context, videoPath string, sourceU if t < 0 { t = 0 } - img, e1 = extractFrameAtTimeWebP(videoPath, t) + img, e1 = extractFrameAtTimeJPG(videoPath, t) } // Letzter Fallback: erstes Frame if e1 != nil || len(img) == 0 { - img, e1 = extractFirstFrameWebPScaled(videoPath, 720, 75) + img, e1 = extractFirstFrameJPGScaled(videoPath, 720, 75) } } @@ -463,7 +463,7 @@ func ensureAssetsForVideoDetailed(ctx context.Context, videoPath string, sourceU cols, rows, _, cellW, cellH := fixedPreviewSpriteLayout() stepSec := previewSpriteStepSeconds(meta.durSec) - if err := generatePreviewSpriteWebP( + if err := generatePreviewSpriteJPG( genCtx, videoPath, spritePath, @@ -474,12 +474,10 @@ func ensureAssetsForVideoDetailed(ctx context.Context, videoPath string, sourceU cellH, ); err != nil { if sfi, statErr := os.Stat(spritePath); statErr == nil && sfi != nil && !sfi.IsDir() && sfi.Size() > 0 { - // Sprite existiert am Ende trotzdem -> Warnung unterdrücken return } - // Optional: nur kurze Meldung statt voller ffmpeg-Ausgabe - //fmt.Printf("⚠️ preview sprite failed for %s\n", videoPath) + fmt.Printf("⚠️ preview sprite failed for %s: %v\n", videoPath, err) return } @@ -581,7 +579,7 @@ func prepareVideoForSplit(ctx context.Context, videoPath, sourceURL, goal string return out, fmt.Errorf("video datei nicht gefunden") } - // 1) Assets sicherstellen (preview.webp / preview.mp4 / preview-sprite.webp / meta.json) + // 1) Assets sicherstellen (preview.jpg / preview.mp4 / preview-sprite.jpg / meta.json) assetsRes, err := ensureAssetsForVideoDetailed(ctx, videoPath, sourceURL, nil) if err != nil { return out, err diff --git a/backend/assets_sprite.go b/backend/assets_sprite.go index 6e791d9..f043275 100644 --- a/backend/assets_sprite.go +++ b/backend/assets_sprite.go @@ -11,6 +11,7 @@ import ( "strings" ) +/* const ( previewSpriteCols = 10 previewSpriteRows = 8 @@ -18,6 +19,15 @@ const ( previewSpriteCellW = 160 previewSpriteCellH = 90 ) +*/ + +const ( + previewSpriteCols = 6 + previewSpriteRows = 5 + previewSpriteFrameCount = previewSpriteCols * previewSpriteRows + previewSpriteCellW = 120 + previewSpriteCellH = 68 +) func fixedPreviewSpriteLayout() (cols, rows, count, cellW, cellH int) { return previewSpriteCols, previewSpriteRows, previewSpriteFrameCount, previewSpriteCellW, previewSpriteCellH @@ -36,9 +46,9 @@ func previewSpriteStepSeconds(durationSec float64) float64 { return step } -// generatePreviewSpriteWebP erzeugt ein statisches WebP-Spritesheet aus einem Video. +// generatePreviewSpriteJPG erzeugt ein statisches JPG-Spritesheet aus einem Video. // ffmpeg muss im PATH verfügbar sein. -func generatePreviewSpriteWebP( +func generatePreviewSpriteJPG( ctx context.Context, videoPath string, outPath string, @@ -52,19 +62,19 @@ func generatePreviewSpriteWebP( outPath = strings.TrimSpace(outPath) if videoPath == "" { - return fmt.Errorf("generatePreviewSpriteWebP: empty videoPath") + return fmt.Errorf("generatePreviewSpriteJPG: empty videoPath") } if outPath == "" { - return fmt.Errorf("generatePreviewSpriteWebP: empty outPath") + return fmt.Errorf("generatePreviewSpriteJPG: empty outPath") } if cols <= 0 || rows <= 0 { - return fmt.Errorf("generatePreviewSpriteWebP: invalid grid %dx%d", cols, rows) + return fmt.Errorf("generatePreviewSpriteJPG: invalid grid %dx%d", cols, rows) } if stepSec <= 0 { - return fmt.Errorf("generatePreviewSpriteWebP: invalid stepSec %.3f", stepSec) + return fmt.Errorf("generatePreviewSpriteJPG: invalid stepSec %.3f", stepSec) } if cellW <= 0 || cellH <= 0 { - return fmt.Errorf("generatePreviewSpriteWebP: invalid cell size %dx%d", cellW, cellH) + return fmt.Errorf("generatePreviewSpriteJPG: invalid cell size %dx%d", cellW, cellH) } if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil { @@ -73,7 +83,7 @@ func generatePreviewSpriteWebP( ext := filepath.Ext(outPath) if ext == "" { - ext = ".webp" + ext = ".jpg" } base := strings.TrimSuffix(outPath, ext) tmpPath := base + ".tmp" + ext @@ -115,14 +125,12 @@ func generatePreviewSpriteWebP( "-i", videoPath, "-an", "-sn", - "-threads", "1", + "-threads", "0", "-vf", vf, "-frames:v", "1", - "-c:v", "libwebp", - "-lossless", "0", - "-compression_level", "3", - "-q:v", "65", - "-f", "webp", + "-c:v", "mjpeg", + "-q:v", "4", + "-f", "image2", tmpPath, ) diff --git a/backend/frontend.go b/backend/frontend.go index 6df1a30..4700b40 100644 --- a/backend/frontend.go +++ b/backend/frontend.go @@ -30,8 +30,6 @@ func makeFrontendHandler() (http.Handler, bool) { return nil, false } - fmt.Println("🖼️ Frontend dist: embedded web/dist") - fileServer := http.FileServer(http.FS(distFS)) h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/backend/live.go b/backend/live.go index 589a3da..f49c9f2 100644 --- a/backend/live.go +++ b/backend/live.go @@ -33,7 +33,7 @@ import ( // - ffmpegPath, previewSem // - notifyJobsChanged() // - assetIDForJob(job *RecordJob) string -// - startLiveThumbWebPLoop(ctx, job) +// - startLiveThumbJPGLoop(ctx, job) // ============================================================ // Allowed files that may be served out of PreviewDir. @@ -122,7 +122,7 @@ func recordPreviewLive(w http.ResponseWriter, r *http.Request) { } // recordPreviewFile serves ONLY the HLS file requests for /api/preview?file=... -// preview.webp bleibt in preview.go (servePreviewWebPAlias). +// preview.jpg bleibt in preview.go (servePreviewJPGAlias). func recordPreviewFile(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodHead { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -337,7 +337,7 @@ func classifyPreviewFFmpegStderr(stderr string) (state string, httpStatus int) { } // startPreviewHLS starts ffmpeg to generate HLS segments in previewDir. -// It also starts your existing live-thumb loop: startLiveThumbWebPLoop(ctx, job). +// It also starts your existing live-thumb loop: startLiveThumbJPGLoop(ctx, job). func startPreviewHLS(ctx context.Context, job *RecordJob, m3u8URL, previewDir, httpCookie, userAgent string) error { if strings.TrimSpace(ffmpegPath) == "" { return fmt.Errorf("kein ffmpeg gefunden – setze FFMPEG_PATH oder lege ffmpeg(.exe) neben das Backend") @@ -440,7 +440,7 @@ func startPreviewHLS(ctx context.Context, job *RecordJob, m3u8URL, previewDir, h jobsMu.Unlock() }() - startLiveThumbWebPLoop(ctx, job) + startLiveThumbJPGLoop(ctx, job) return nil } diff --git a/backend/main.go b/backend/main.go index 1533931..1024bc4 100644 --- a/backend/main.go +++ b/backend/main.go @@ -71,10 +71,10 @@ type RecordJob struct { PreviewStateMsg string `json:"previewStateMsg,omitempty"` // kurze Info // Thumbnail cache (verhindert, dass pro HTTP-Request ffmpeg läuft) - previewMu sync.Mutex `json:"-"` - previewWebp []byte `json:"-"` - previewWebpAt time.Time `json:"-"` - previewGen bool `json:"-"` + previewMu sync.Mutex `json:"-"` + previewJPG []byte `json:"-"` + previewJPGAt time.Time `json:"-"` + previewGen bool `json:"-"` PreviewM3U8 string `json:"-"` // HLS url, die ffmpeg inputt PreviewCookie string `json:"-"` // Cookie header (falls nötig) @@ -484,19 +484,21 @@ func initFFmpegSemaphores() { genSem = NewDynSem(genN, genCap) durSem = NewDynSem(durN, durCap) - fmt.Printf( - "🔧 semaphores(init): preview=%d/%d thumb=%d/%d gen=%d/%d dur=%d/%d (cpu=%d)\n", - previewSem.Max(), previewSem.Cap(), - thumbSem.Max(), thumbSem.Cap(), - genSem.Max(), genSem.Cap(), - durSem.Max(), durSem.Cap(), - cpu, - ) + /* + fmt.Printf( + "🔧 semaphores(init): preview=%d/%d thumb=%d/%d gen=%d/%d dur=%d/%d (cpu=%d)\n", + previewSem.Max(), previewSem.Cap(), + thumbSem.Max(), thumbSem.Cap(), + genSem.Max(), genSem.Cap(), + durSem.Max(), durSem.Cap(), + cpu, + ) - fmt.Printf( - "🔧 semaphores: preview=%d thumb=%d gen=%d dur=%d (cpu=%d)\n", - previewN, thumbN, genN, durN, cpu, - ) + fmt.Printf( + "🔧 semaphores: preview=%d thumb=%d gen=%d dur=%d (cpu=%d)\n", + previewN, thumbN, genN, durN, cpu, + ) + */ } func startAdaptiveSemController(ctx context.Context) { @@ -1371,9 +1373,9 @@ func generatedMetaRoot() (string, error) { return resolvePathRelativeToApp(filepath.Join("generated", "meta")) } -// generatedThumbWebPFile gibt den Pfad zu generated//preview.webp zurück. +// generatedThumbJPGFile gibt den Pfad zu generated//preview.jpg zurück. // assetID darf "HOT " enthalten; wird entfernt und wie überall sonst sanitisiert. -func generatedThumbWebPFile(assetID string) (string, error) { +func generatedThumbJPGFile(assetID string) (string, error) { assetID = stripHotPrefix(strings.TrimSpace(assetID)) if assetID == "" { return "", fmt.Errorf("empty assetID") @@ -1391,7 +1393,7 @@ func generatedThumbWebPFile(assetID string) (string, error) { return "", fmt.Errorf("ensureGeneratedDir: %w", err) } - return filepath.Join(dir, "preview.webp"), nil + return filepath.Join(dir, "preview.jpg"), nil } // Legacy (falls noch alte Assets liegen): diff --git a/backend/meta.go b/backend/meta.go index 5d5b1df..c89e657 100644 --- a/backend/meta.go +++ b/backend/meta.go @@ -525,7 +525,7 @@ func generatedThumbFile(id string) (string, error) { if err != nil { return "", err } - return filepath.Join(dir, "preview.webp"), nil + return filepath.Join(dir, "preview.jpg"), nil } func generatedPreviewFile(id string) (string, error) { @@ -541,7 +541,7 @@ func generatedPreviewSpriteFile(id string) (string, error) { if err != nil { return "", err } - return filepath.Join(dir, "preview-sprite.webp"), nil + return filepath.Join(dir, "preview-sprite.jpg"), nil } func ensureGeneratedDirs() error { diff --git a/backend/nsfw_detector.go b/backend/nsfw_detector.go index 1f01681..7c29a52 100644 --- a/backend/nsfw_detector.go +++ b/backend/nsfw_detector.go @@ -138,10 +138,6 @@ func initNSFWDetector() error { globalNSFW.session = session globalNSFW.initialized = true - fmt.Println("[NSFW] ONNX detector bereit") - fmt.Println("[NSFW] model:", modelPath) - fmt.Println("[NSFW] dll:", dllPath) - return nil } diff --git a/backend/preview.go b/backend/preview.go index bd1bf5b..548c32d 100644 --- a/backend/preview.go +++ b/backend/preview.go @@ -44,7 +44,7 @@ import ( // - atomicWriteFile, ensureGeneratedDirs, ensureGeneratedDir // - durationSecondsCached, parseFFmpegOutTime, ffmpegInputTol // - jobs, jobsMu, RecordJob, previewSem, thumbSem, JobRunning, notifyJobsChanged -// - sanitizeID, findFinishedFileByID, stripHotPrefix, assetIDForJob, generatedThumbWebPFile +// - sanitizeID, findFinishedFileByID, stripHotPrefix, assetIDForJob, generatedThumbJPGFile // Bitte diese Abhängigkeiten NICHT löschen – preview.go nutzt sie. // ============================================================ @@ -453,8 +453,6 @@ func detectImageExt(contentType string, b []byte) (ext string, ct string) { return ".jpg", "image/jpeg" case strings.Contains(ct, "image/png"): return ".png", "image/png" - case strings.Contains(ct, "image/webp"): - return ".webp", "image/webp" case strings.Contains(ct, "image/gif"): return ".gif", "image/gif" } @@ -465,7 +463,7 @@ func detectImageExt(contentType string, b []byte) (ext string, ct string) { return ".png", "image/png" } if len(b) >= 12 && string(b[:4]) == "RIFF" && string(b[8:12]) == "WEBP" { - return ".webp", "image/webp" + return ".jpg", "image/jpeg" } if len(b) >= 6 && (string(b[:6]) == "GIF87a" || string(b[:6]) == "GIF89a") { return ".gif", "image/gif" @@ -492,7 +490,7 @@ func findExistingCoverFile(key string) (string, os.FileInfo, bool) { if err != nil || strings.TrimSpace(root) == "" { return "", nil, false } - ext := []string{".jpg", ".png", ".webp", ".gif"} + ext := []string{".jpg", ".png", ".gif"} for _, e := range ext { p := filepath.Join(root, key+e) if fi, err := os.Stat(p); err == nil && !fi.IsDir() && fi.Size() > 0 { @@ -545,8 +543,6 @@ func downloadBytes(ctx context.Context, rawURL string, ua string) ([]byte, strin ct = "image/jpeg" case ".png": ct = "image/png" - case ".webp": - ct = "image/webp" case ".gif": ct = "image/gif" } @@ -721,7 +717,7 @@ func generatedCoverInfoList(w http.ResponseWriter, r *http.Request) { isCoverExt := func(ext string) bool { switch strings.ToLower(ext) { - case ".jpg", ".jpeg", ".png", ".webp", ".gif": + case ".jpg", ".jpeg", ".png", ".gif": return true default: return false @@ -868,8 +864,8 @@ func generatedCover(w http.ResponseWriter, r *http.Request) { switch ext { case ".png": w.Header().Set("Content-Type", "image/png") - case ".webp": - w.Header().Set("Content-Type", "image/webp") + case ".jpg", ".jpeg": + w.Header().Set("Content-Type", "image/jpeg") case ".gif": w.Header().Set("Content-Type", "image/gif") default: @@ -966,8 +962,8 @@ func generatedCover(w http.ResponseWriter, r *http.Request) { switch ext2 { case ".png": w.Header().Set("Content-Type", "image/png") - case ".webp": - w.Header().Set("Content-Type", "image/webp") + case ".jpg": + w.Header().Set("Content-Type", "image/jpeg") case ".gif": w.Header().Set("Content-Type", "image/gif") default: @@ -1050,7 +1046,7 @@ func generatedCover(w http.ResponseWriter, r *http.Request) { } root, _ := coversRoot() - for _, e := range []string{".jpg", ".png", ".webp", ".gif"} { + for _, e := range []string{".jpg", ".png", ".gif"} { _ = os.Remove(filepath.Join(root, key+e)) } _ = os.Remove(filepath.Join(root, key+".info.json")) @@ -1163,15 +1159,15 @@ func servePreviewStatusSVG(w http.ResponseWriter, label string, status int) { } // ============================================================ -// WebP extraction + preview endpoint +// JPG extraction + preview endpoint // Route: -// - /api/preview?id= (returns preview.webp / 204 / svg) -// - /api/preview?id=&file=preview.webp +// - /api/preview?id= (returns preview.jpg / 204 / svg) +// - /api/preview?id=&file=preview.jpg // ============================================================ -// --- WebP extraction helpers --- +// --- JPG extraction helpers --- -func extractLastFrameWebP(path string) ([]byte, error) { +func extractLastFrameJPG(path string) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() @@ -1180,32 +1176,16 @@ func extractLastFrameWebP(path string) ([]byte, error) { ffmpegPath, "-hide_banner", "-loglevel", "error", - - // relativ zum Dateiende suchen "-sseof", "-0.25", - "-i", path, - - // nur den ersten Video-Stream verwenden "-map", "0:v:0", - - // alles andere hart abschalten "-an", "-sn", "-dn", - - // genau 1 Frame "-frames:v", "1", - - // schneller skalieren "-vf", "scale=720:-2:flags=fast_bilinear", - - // WebP: Qualität + schnellerer Encode - "-vcodec", "libwebp", - "-quality", "75", - "-compression_level", "2", - "-preset", "photo", - + "-vcodec", "mjpeg", + "-q:v", "4", "-f", "image2pipe", "pipe:1", ) @@ -1217,20 +1197,20 @@ func extractLastFrameWebP(path string) ([]byte, error) { if err := cmd.Run(); err != nil { if ctx.Err() == context.DeadlineExceeded { - return nil, fmt.Errorf("ffmpeg last-frame webp: timeout") + return nil, fmt.Errorf("ffmpeg last-frame jpg: timeout") } - return nil, fmt.Errorf("ffmpeg last-frame webp: %w (%s)", err, strings.TrimSpace(stderr.String())) + return nil, fmt.Errorf("ffmpeg last-frame jpg: %w (%s)", err, strings.TrimSpace(stderr.String())) } b := out.Bytes() if len(b) == 0 { - return nil, fmt.Errorf("ffmpeg last-frame webp: empty output") + return nil, fmt.Errorf("ffmpeg last-frame jpg: empty output") } return b, nil } -func extractFrameAtTimeWebP(path string, seconds float64) ([]byte, error) { +func extractFrameAtTimeJPG(path string, seconds float64) ([]byte, error) { if seconds < 0 { seconds = 0 } @@ -1243,26 +1223,27 @@ func extractFrameAtTimeWebP(path string, seconds float64) ([]byte, error) { "-i", path, "-frames:v", "1", "-vf", "scale=720:-2", - "-quality", "75", + "-vcodec", "mjpeg", + "-q:v", "4", "-f", "image2pipe", - "-vcodec", "libwebp", "pipe:1", ) + var out bytes.Buffer var stderr bytes.Buffer cmd.Stdout = &out cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("ffmpeg frame-at-time webp: %w (%s)", err, strings.TrimSpace(stderr.String())) + return nil, fmt.Errorf("ffmpeg frame-at-time jpg: %w (%s)", err, strings.TrimSpace(stderr.String())) } b := out.Bytes() if len(b) == 0 { - return nil, fmt.Errorf("ffmpeg frame-at-time webp: empty output") + return nil, fmt.Errorf("ffmpeg frame-at-time jpg: empty output") } return b, nil } -func extractLastFrameWebPScaled(path string, width int, quality int) ([]byte, error) { +func extractLastFrameJPGScaled(path string, width int, quality int) ([]byte, error) { if width <= 0 { width = 320 } @@ -1270,6 +1251,15 @@ func extractLastFrameWebPScaled(path string, width int, quality int) ([]byte, er quality = 70 } + qv := "5" + if quality >= 80 { + qv = "3" + } else if quality >= 65 { + qv = "5" + } else { + qv = "7" + } + cmd := exec.Command( ffmpegPath, "-hide_banner", "-loglevel", "error", @@ -1277,9 +1267,9 @@ func extractLastFrameWebPScaled(path string, width int, quality int) ([]byte, er "-i", path, "-frames:v", "1", "-vf", fmt.Sprintf("scale=%d:-2", width), - "-quality", strconv.Itoa(quality), + "-vcodec", "mjpeg", + "-q:v", qv, "-f", "image2pipe", - "-vcodec", "libwebp", "pipe:1", ) @@ -1288,16 +1278,16 @@ func extractLastFrameWebPScaled(path string, width int, quality int) ([]byte, er cmd.Stdout = &out cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("ffmpeg last-frame scaled webp: %w (%s)", err, strings.TrimSpace(stderr.String())) + return nil, fmt.Errorf("ffmpeg last-frame scaled jpg: %w (%s)", err, strings.TrimSpace(stderr.String())) } b := out.Bytes() if len(b) == 0 { - return nil, fmt.Errorf("ffmpeg last-frame scaled webp: empty output") + return nil, fmt.Errorf("ffmpeg last-frame scaled jpg: empty output") } return b, nil } -func extractFirstFrameWebPScaled(path string, width int, quality int) ([]byte, error) { +func extractFirstFrameJPGScaled(path string, width int, quality int) ([]byte, error) { if width <= 0 { width = 320 } @@ -1312,9 +1302,9 @@ func extractFirstFrameWebPScaled(path string, width int, quality int) ([]byte, e "-i", path, "-frames:v", "1", "-vf", fmt.Sprintf("scale=%d:-2", width), - "-quality", strconv.Itoa(quality), + "-vcodec", "mjpeg", + "-q:v", "5", "-f", "image2pipe", - "-vcodec", "libwebp", "pipe:1", ) @@ -1323,11 +1313,11 @@ func extractFirstFrameWebPScaled(path string, width int, quality int) ([]byte, e cmd.Stdout = &out cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("ffmpeg first-frame scaled webp: %w (%s)", err, strings.TrimSpace(stderr.String())) + return nil, fmt.Errorf("ffmpeg first-frame scaled jpg: %w (%s)", err, strings.TrimSpace(stderr.String())) } b := out.Bytes() if len(b) == 0 { - return nil, fmt.Errorf("ffmpeg first-frame scaled webp: empty output") + return nil, fmt.Errorf("ffmpeg first-frame scaled jpg: empty output") } return b, nil } @@ -1356,31 +1346,31 @@ func latestPreviewSegment(previewDir string) (string, error) { return filepath.Join(previewDir, best), nil } -func extractLastFrameFromPreviewDirThumbWebP(previewDir string) ([]byte, error) { +func extractLastFrameFromPreviewDirThumbJPG(previewDir string) ([]byte, error) { seg, err := latestPreviewSegment(previewDir) if err != nil { return nil, err } - img, err := extractLastFrameWebPScaled(seg, 320, 70) + img, err := extractLastFrameJPGScaled(seg, 320, 70) if err == nil && len(img) > 0 { return img, nil } - return extractFirstFrameWebPScaled(seg, 320, 70) + return extractFirstFrameJPGScaled(seg, 320, 70) } -func extractLastFrameFromPreviewDirWebP(previewDir string) ([]byte, error) { +func extractLastFrameFromPreviewDirJPG(previewDir string) ([]byte, error) { seg, err := latestPreviewSegment(previewDir) if err != nil { return nil, err } - img, err := extractLastFrameWebP(seg) + img, err := extractLastFrameJPG(seg) if err != nil { - return extractFirstFrameWebPScaled(seg, 720, 75) + return extractFirstFrameJPGScaled(seg, 720, 75) } return img, nil } -func serveLivePreviewWebPFile(w http.ResponseWriter, r *http.Request, path string) { +func serveLivePreviewJPGFile(w http.ResponseWriter, r *http.Request, path string) { f, err := os.Open(path) if err != nil { http.NotFound(w, r) @@ -1394,12 +1384,12 @@ func serveLivePreviewWebPFile(w http.ResponseWriter, r *http.Request, path strin return } - w.Header().Set("Content-Type", "image/webp") + w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "no-store") - http.ServeContent(w, r, "preview.webp", st.ModTime(), f) + http.ServeContent(w, r, "preview.jpg", st.ModTime(), f) } -func servePreviewWebPFile(w http.ResponseWriter, r *http.Request, path string) { +func servePreviewJPGFile(w http.ResponseWriter, r *http.Request, path string) { f, err := os.Open(path) if err != nil { http.NotFound(w, r) @@ -1413,35 +1403,35 @@ func servePreviewWebPFile(w http.ResponseWriter, r *http.Request, path string) { return } - w.Header().Set("Content-Type", "image/webp") + w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "public, max-age=600") http.ServeContent(w, r, filepath.Base(path), st.ModTime(), f) } -func servePreviewWebPBytes(w http.ResponseWriter, b []byte) { +func servePreviewJPGBytes(w http.ResponseWriter, b []byte) { if len(b) == 0 { w.WriteHeader(http.StatusNoContent) return } - w.Header().Set("Content-Type", "image/webp") + w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "public, max-age=60") w.WriteHeader(http.StatusOK) _, _ = w.Write(b) } -func serveLivePreviewWebPBytes(w http.ResponseWriter, b []byte) { +func serveLivePreviewJPGBytes(w http.ResponseWriter, b []byte) { if len(b) == 0 { w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusNoContent) return } - w.Header().Set("Content-Type", "image/webp") + w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusOK) _, _ = w.Write(b) } -func servePreviewWebPAlias(w http.ResponseWriter, r *http.Request, id string) { +func servePreviewJPGAlias(w http.ResponseWriter, r *http.Request, id string) { jobsMu.Lock() job := jobs[id] jobsMu.Unlock() @@ -1449,12 +1439,12 @@ func servePreviewWebPAlias(w http.ResponseWriter, r *http.Request, id string) { if job != nil { assetID := assetIDForJob(job) if assetID != "" { - if webpPath, err := generatedThumbWebPFile(assetID); err == nil { - if st, err := os.Stat(webpPath); err == nil && !st.IsDir() && st.Size() > 0 { + if jpgPath, err := generatedThumbJPGFile(assetID); err == nil { + if st, err := os.Stat(jpgPath); err == nil && !st.IsDir() && st.Size() > 0 { if job.Status == JobRunning { - serveLivePreviewWebPFile(w, r, webpPath) + serveLivePreviewJPGFile(w, r, jpgPath) } else { - servePreviewWebPFile(w, r, webpPath) + servePreviewJPGFile(w, r, jpgPath) } return } @@ -1463,10 +1453,10 @@ func servePreviewWebPAlias(w http.ResponseWriter, r *http.Request, id string) { if job.Status == JobRunning { job.previewMu.Lock() - cached := job.previewWebp + cached := job.previewJPG job.previewMu.Unlock() if len(cached) > 0 { - serveLivePreviewWebPBytes(w, cached) + serveLivePreviewJPGBytes(w, cached) return } } @@ -1480,9 +1470,9 @@ func servePreviewWebPAlias(w http.ResponseWriter, r *http.Request, id string) { http.NotFound(w, r) return } - if webpPath, err := generatedThumbWebPFile(assetID); err == nil { - if st, err := os.Stat(webpPath); err == nil && !st.IsDir() && st.Size() > 0 { - servePreviewWebPFile(w, r, webpPath) + if jpgPath, err := generatedThumbJPGFile(assetID); err == nil { + if st, err := os.Stat(jpgPath); err == nil && !st.IsDir() && st.Size() > 0 { + servePreviewJPGFile(w, r, jpgPath) return } } @@ -1513,9 +1503,9 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri if file := strings.TrimSpace(r.URL.Query().Get("file")); file != "" { low := strings.ToLower(strings.TrimSpace(file)) - // ✅ preview.webp weiterhin hier behandeln - if low == "preview.webp" { - servePreviewWebPAlias(w, r, id) + // ✅ preview.jpg weiterhin hier behandeln + if low == "preview.jpg" { + servePreviewJPGAlias(w, r, id) return } @@ -1524,7 +1514,7 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri return } - // WebP preview (running jobs have live thumb behavior) + // JPG preview (running jobs have live thumb behavior) jobsMu.Lock() job, ok := jobs[id] jobsMu.Unlock() @@ -1533,9 +1523,9 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri if job.Status == JobRunning { assetID := assetIDForJob(job) if assetID != "" { - if webpPath, err := generatedThumbWebPFile(assetID); err == nil { - if st, err := os.Stat(webpPath); err == nil && !st.IsDir() && st.Size() > 0 { - serveLivePreviewWebPFile(w, r, webpPath) + if jpgPath, err := generatedThumbJPGFile(assetID); err == nil { + if st, err := os.Stat(jpgPath); err == nil && !st.IsDir() && st.Size() > 0 { + serveLivePreviewJPGFile(w, r, jpgPath) return } } @@ -1543,8 +1533,8 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri } job.previewMu.Lock() - cached := job.previewWebp - cachedAt := job.previewWebpAt + cached := job.previewJPG + cachedAt := job.previewJPGAt fresh := len(cached) > 0 && !cachedAt.IsZero() && time.Since(cachedAt) < 8*time.Second if !fresh && !job.previewGen { @@ -1561,7 +1551,7 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri previewDir := strings.TrimSpace(j.PreviewDir) if previewDir != "" { - img, genErr = extractLastFrameFromPreviewDirWebP(previewDir) + img, genErr = extractLastFrameFromPreviewDirJPG(previewDir) } if genErr != nil || len(img) == 0 { @@ -1574,9 +1564,9 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri } } if fi, err := os.Stat(outPath); err == nil && !fi.IsDir() && fi.Size() > 0 { - img, genErr = extractLastFrameWebP(outPath) + img, genErr = extractLastFrameJPG(outPath) if genErr != nil { - img, _ = extractFirstFrameWebPScaled(outPath, 720, 75) + img, _ = extractFirstFrameJPGScaled(outPath, 720, 75) } } } @@ -1584,8 +1574,8 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri if len(img) > 0 { j.previewMu.Lock() - j.previewWebp = img - j.previewWebpAt = time.Now() + j.previewJPG = img + j.previewJPGAt = time.Now() j.previewMu.Unlock() } }(job) @@ -1595,7 +1585,7 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri job.previewMu.Unlock() if len(out) > 0 { - serveLivePreviewWebPBytes(w, out) + serveLivePreviewJPGBytes(w, out) return } @@ -1621,7 +1611,7 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri servePreviewForFinishedFile(w, r, id) } -func updateLiveThumbWebPOnce(ctx context.Context, job *RecordJob) { +func updateLiveThumbJPGOnce(ctx context.Context, job *RecordJob) { jobsMu.Lock() status := job.Status previewDir := job.PreviewDir @@ -1633,7 +1623,7 @@ func updateLiveThumbWebPOnce(ctx context.Context, job *RecordJob) { } assetID := assetIDForJob(job) - thumbPath, err := generatedThumbWebPFile(assetID) + thumbPath, err := generatedThumbJPGFile(assetID) if err != nil { return } @@ -1655,12 +1645,12 @@ func updateLiveThumbWebPOnce(ctx context.Context, job *RecordJob) { var img []byte if previewDir != "" { - if b, err := extractLastFrameFromPreviewDirThumbWebP(previewDir); err == nil && len(b) > 0 { + if b, err := extractLastFrameFromPreviewDirThumbJPG(previewDir); err == nil && len(b) > 0 { img = b } } if len(img) == 0 && out != "" { - if b, err := extractLastFrameWebPScaled(out, 320, 70); err == nil && len(b) > 0 { + if b, err := extractLastFrameJPGScaled(out, 320, 70); err == nil && len(b) > 0 { img = b } } @@ -1670,7 +1660,7 @@ func updateLiveThumbWebPOnce(ctx context.Context, job *RecordJob) { _ = atomicWriteFile(thumbPath, img) } -func startLiveThumbWebPLoop(ctx context.Context, job *RecordJob) { +func startLiveThumbJPGLoop(ctx context.Context, job *RecordJob) { jobsMu.Lock() if job.LiveThumbStarted { jobsMu.Unlock() @@ -1680,7 +1670,7 @@ func startLiveThumbWebPLoop(ctx context.Context, job *RecordJob) { jobsMu.Unlock() go func() { - updateLiveThumbWebPOnce(ctx, job) + updateLiveThumbJPGOnce(ctx, job) for { select { case <-ctx.Done(): @@ -1692,7 +1682,7 @@ func startLiveThumbWebPLoop(ctx context.Context, job *RecordJob) { if st != JobRunning { return } - updateLiveThumbWebPOnce(ctx, job) + updateLiveThumbJPGOnce(ctx, job) } } }() @@ -1734,17 +1724,17 @@ func servePreviewForFinishedFile(w http.ResponseWriter, r *http.Request, id stri sec = 0 } - img, err := extractFrameAtTimeWebP(outPath, sec) + img, err := extractFrameAtTimeJPG(outPath, sec) if err == nil && len(img) > 0 { - servePreviewWebPBytes(w, img) + servePreviewJPGBytes(w, img) return } } } - thumbPath := filepath.Join(assetDir, "preview.webp") + thumbPath := filepath.Join(assetDir, "preview.jpg") if fi, err := os.Stat(thumbPath); err == nil && !fi.IsDir() && fi.Size() > 0 { - servePreviewWebPFile(w, r, thumbPath) + servePreviewJPGFile(w, r, thumbPath) return } @@ -1752,7 +1742,7 @@ func servePreviewForFinishedFile(w http.ResponseWriter, r *http.Request, id stri defer cancel() // ✅ Immer letztes Frame bevorzugen - img, err := extractLastFrameWebP(outPath) + img, err := extractLastFrameJPG(outPath) if err != nil || len(img) == 0 { // Fallback: kurz vor Ende, falls Duration verfügbar @@ -1761,12 +1751,12 @@ func servePreviewForFinishedFile(w http.ResponseWriter, r *http.Request, id stri if t < 0 { t = 0 } - img, err = extractFrameAtTimeWebP(outPath, t) + img, err = extractFrameAtTimeJPG(outPath, t) } // Letzter Fallback: erstes Frame if err != nil || len(img) == 0 { - img, err = extractFirstFrameWebPScaled(outPath, 720, 75) + img, err = extractFirstFrameJPGScaled(outPath, 720, 75) if err != nil || len(img) == 0 { http.Error(w, "konnte preview nicht erzeugen", http.StatusInternalServerError) return @@ -1775,7 +1765,7 @@ func servePreviewForFinishedFile(w http.ResponseWriter, r *http.Request, id stri } _ = atomicWriteFile(thumbPath, img) - servePreviewWebPBytes(w, img) + servePreviewJPGBytes(w, img) } // ============================================================ diff --git a/backend/record.go b/backend/record.go index 4adb6e7..cd8dcff 100644 --- a/backend/record.go +++ b/backend/record.go @@ -320,7 +320,7 @@ func previewSpriteTruthForID(id string) previewSpriteMetaResp { } genDir := filepath.Dir(metaPath) - spriteFile := filepath.Join(genDir, "preview-sprite.webp") + spriteFile := filepath.Join(genDir, "preview-sprite.jpg") fi, err := os.Stat(spriteFile) if err != nil || fi == nil || fi.IsDir() || fi.Size() <= 0 { diff --git a/backend/recorder.go b/backend/recorder.go index 2364934..7ce493f 100644 --- a/backend/recorder.go +++ b/backend/recorder.go @@ -230,7 +230,7 @@ func recordPreviewSprite(w http.ResponseWriter, r *http.Request) { return } - spritePath := filepath.Join(dir, "preview-sprite.webp") + spritePath := filepath.Join(dir, "preview-sprite.jpg") fi, err := os.Stat(spritePath) if err != nil || fi.IsDir() || fi.Size() <= 0 { @@ -245,11 +245,11 @@ func recordPreviewSprite(w http.ResponseWriter, r *http.Request) { } defer f.Close() - w.Header().Set("Content-Type", "image/webp") + w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Cache-Control", "private, max-age=31536000, immutable") w.Header().Set("X-Content-Type-Options", "nosniff") - http.ServeContent(w, r, "preview-sprite.webp", fi.ModTime(), f) + http.ServeContent(w, r, "preview-sprite.jpg", fi.ModTime(), f) } // ---------------- Start + run job ---------------- @@ -703,30 +703,39 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) { setPhase("analyze", 5) { actx, cancel := context.WithTimeout(ctx, 45*time.Second) + defer cancel() - durationSec, _ := durationSecondsForAnalyze(actx, out) - hits, aerr := analyzeVideoFromSprite(actx, out, "nsfw") - if aerr != nil { - fmt.Println("⚠️ postwork analyze:", aerr) + id := assetIDFromVideoPath(out) + if strings.TrimSpace(id) == "" { + fmt.Println("⚠️ postwork analyze: keine asset id ableitbar") } else { - setPhase("analyze", 65) + ps := previewSpriteTruthForID(id) + if !ps.Exists { + fmt.Println("⚠️ postwork analyze: preview-sprite.jpg nicht gefunden") + } else { + durationSec, _ := durationSecondsForAnalyze(actx, out) + hits, aerr := analyzeVideoFromSprite(actx, out, "nsfw") + if aerr != nil { + fmt.Println("⚠️ postwork analyze:", aerr) + } else { + setPhase("analyze", 65) - segments := buildSegmentsFromAnalyzeHits(hits, durationSec) + segments := buildSegmentsFromAnalyzeHits(hits, durationSec) - ai := &aiAnalysisMeta{ - Goal: "nsfw", - Mode: "sprite", - Hits: hits, - Segments: segments, - AnalyzedAtUnix: time.Now().Unix(), - } + ai := &aiAnalysisMeta{ + Goal: "nsfw", + Mode: "sprite", + Hits: hits, + Segments: segments, + AnalyzedAtUnix: time.Now().Unix(), + } - if werr := writeVideoAIForFile(actx, out, job.SourceURL, ai); werr != nil { - fmt.Println("⚠️ writeVideoAIForFile:", werr) + if werr := writeVideoAIForFile(actx, out, job.SourceURL, ai); werr != nil { + fmt.Println("⚠️ writeVideoAIForFile:", werr) + } + } } } - - cancel() } setPhase("analyze", 100) diff --git a/backend/recorder_settings.json b/backend/recorder_settings.json index 97405df..736a221 100644 --- a/backend/recorder_settings.json +++ b/backend/recorder_settings.json @@ -15,5 +15,5 @@ "teaserPlayback": "hover", "teaserAudio": false, "enableNotifications": true, - "encryptedCookies": "3EPvjFs7b4JIdKUT3G2fOZKc26YmYL283VVHmG+dCLAUe+xURUkM0rZMCrf8Ug7eyXZOreLItE09FSCZrA3afNgmHg5c648hhvYhkv/mW7J8ap4tMz1m8ahcvcfoLhrx5AqU4MWXnqz+VHHglqkfPn9aFcrgFnWbOPHJ1A3S77cs2gWR0/shqn3l8nk6HmIWqJ1TnAA6z2CYDngB27sv/NflLKoujezlWitEa8wEpEW8GDSEtPjpT7X9L24wP4TK/TnxZUovaRXDDbboebk2KeKP04C5tWhhpIfKl3/ipf9dPgHdV4jLheFyczMRZN5Z6yF5WRn3NgDbdCcoldRwqgTwv1NgLri8nJKp4SGmRpGFrbq6m7/26muyGbTzsU3tniae6iYHbYrPz0pMOBLcFPxnil4yT0Xgnph+P9EYYWJxtjUXi7nsiREjHBxqU/OSogavsOjlFqJgWBBCL705R2Fap0VjlgWtJEXKu+vAlexX873uoeFzFw9niwJlNRFKJtGMjJGYE5c=" + "encryptedCookies": "" } diff --git a/backend/settings.go b/backend/settings.go index c980a5b..a585c7d 100644 --- a/backend/settings.go +++ b/backend/settings.go @@ -171,10 +171,10 @@ func loadSettings() { // ffmpeg-Pfad anhand Settings/Env/PATH bestimmen ffmpegPath = detectFFmpegPath() - fmt.Println("🔍 ffmpegPath:", ffmpegPath) + //fmt.Println("🔍 ffmpegPath:", ffmpegPath) ffprobePath = detectFFprobePath() - fmt.Println("🔍 ffprobePath:", ffprobePath) + //fmt.Println("🔍 ffprobePath:", ffprobePath) } diff --git a/frontend/src/components/ui/CategoriesTab.tsx b/frontend/src/components/ui/CategoriesTab.tsx index 1893b0e..c0ad957 100644 --- a/frontend/src/components/ui/CategoriesTab.tsx +++ b/frontend/src/components/ui/CategoriesTab.tsx @@ -376,7 +376,7 @@ export default function CategoriesTab() { } } - const thumb = `/generated/meta/${encodeURIComponent(id)}/preview.webp` + const thumb = `/generated/meta/${encodeURIComponent(id)}/preview.jpg` const model = modelKeyFromFilename(pick) await ensureCover(r.tag, thumb, model, true) diff --git a/frontend/src/components/ui/CookieModal.tsx b/frontend/src/components/ui/CookieModal.tsx index 6dcf3d2..2a7a3ae 100644 --- a/frontend/src/components/ui/CookieModal.tsx +++ b/frontend/src/components/ui/CookieModal.tsx @@ -1,3 +1,5 @@ +// frontend\src\components\ui\CookieModal.tsx + 'use client' import { Dialog } from '@headlessui/react' @@ -24,7 +26,6 @@ export default function CookieModal({ const [cookies, setCookies] = useState([]) const wasOpen = useRef(false) - // ✅ Beim Öffnen: Inputs resetten UND Cookies aus Props übernehmen useEffect(() => { if (open && !wasOpen.current) { setName('') @@ -59,72 +60,167 @@ export default function CookieModal({ return ( - ) -} +} \ No newline at end of file diff --git a/frontend/src/components/ui/Downloads.tsx b/frontend/src/components/ui/Downloads.tsx index 56e2050..73104ee 100644 --- a/frontend/src/components/ui/Downloads.tsx +++ b/frontend/src/components/ui/Downloads.tsx @@ -223,20 +223,20 @@ const absUrlMaybe = (u?: string | null): string => { return `/${s}` } -const jobThumbsWebpCandidates = (job: RecordJob): string[] => { +const jobThumbsJPGCandidates = (job: RecordJob): string[] => { const j = job as any const direct = [ - j.thumbsWebpUrl, + j.thumbsJPGUrl, j.thumbsUrl, j.previewThumbsUrl, j.thumbnailSheetUrl, ] const base = [ - j.previewBaseUrl ? `${String(j.previewBaseUrl).replace(/\/+$/, '')}/preview.webp` : '', - j.assetBaseUrl ? `${String(j.assetBaseUrl).replace(/\/+$/, '')}/preview.webp` : '', - j.thumbsBaseUrl ? `${String(j.thumbsBaseUrl).replace(/\/+$/, '')}/preview.webp` : '', + j.previewBaseUrl ? `${String(j.previewBaseUrl).replace(/\/+$/, '')}/preview.jpg` : '', + j.assetBaseUrl ? `${String(j.assetBaseUrl).replace(/\/+$/, '')}/preview.jpg` : '', + j.thumbsBaseUrl ? `${String(j.thumbsBaseUrl).replace(/\/+$/, '')}/preview.jpg` : '', ] return [...direct, ...base] @@ -656,7 +656,7 @@ function DownloadsCardRow({ fastRetryMs={1000} fastRetryMax={25} fastRetryWindowMs={60_000} - thumbsCandidates={jobThumbsWebpCandidates(j)} + thumbsCandidates={jobThumbsJPGCandidates(j)} className="w-full h-full" /> @@ -1297,7 +1297,7 @@ export default function Downloads({ fastRetryMs={1000} fastRetryMax={25} fastRetryWindowMs={60_000} - thumbsCandidates={jobThumbsWebpCandidates(j)} + thumbsCandidates={jobThumbsJPGCandidates(j)} className="w-full h-full" /> diff --git a/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx b/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx index 7b051c3..d645ce0 100644 --- a/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx +++ b/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx @@ -587,7 +587,7 @@ export default function FinishedDownloadsGalleryView({ {/* Footer / Meta */} -
+
{/* ✅ stashapp-like: Dateiname zuerst */}
@@ -641,7 +641,7 @@ export default function FinishedDownloadsGalleryView({ {/* Actions (wie CardView: im Footer statt im Video) */}
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} > diff --git a/frontend/src/components/ui/FinishedDownloadsTableView.tsx b/frontend/src/components/ui/FinishedDownloadsTableView.tsx index 6bb31b4..9089e1a 100644 --- a/frontend/src/components/ui/FinishedDownloadsTableView.tsx +++ b/frontend/src/components/ui/FinishedDownloadsTableView.tsx @@ -342,7 +342,7 @@ export default function FinishedDownloadsTableView({
{tags.length > 0 ? ( -
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}> +
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}> +
{isLoading && rows.length === 0 ? ( -
+
Lade…
diff --git a/frontend/src/components/ui/ModelDetails.tsx b/frontend/src/components/ui/ModelDetails.tsx index 169baaf..095f0d3 100644 --- a/frontend/src/components/ui/ModelDetails.tsx +++ b/frontend/src/components/ui/ModelDetails.tsx @@ -1160,17 +1160,7 @@ export default function ModelDetails({ /> {/* Pills */} -
- {showPill ? ( - - {showPill} - - ) : null} - +
{effectivePresenceLabel ? ( @@ -1260,9 +1252,11 @@ export default function ModelDetails({ handleToggleFavoriteModel() }} className={cn( - 'inline-flex items-center justify-center rounded-full p-1.5 ring-1 ring-inset backdrop-blur', - 'cursor-pointer hover:scale-[1.03] active:scale-[0.98] transition', - model?.favorite ? 'bg-amber-500/25 ring-amber-200/30' : 'bg-black/20 ring-white/15' + 'inline-flex items-center justify-center rounded-full p-1.5 ring-1 backdrop-blur shadow-sm', + 'cursor-pointer transition hover:scale-[1.03] active:scale-[0.98]', + model?.favorite + ? 'bg-amber-100/95 text-amber-700 ring-amber-200 hover:bg-amber-200/95 dark:bg-amber-500/25 dark:text-amber-200 dark:ring-amber-200/30 dark:hover:bg-amber-500/30' + : 'bg-white/90 text-gray-700 ring-gray-200 hover:bg-white dark:bg-black/20 dark:text-white dark:ring-white/15 dark:hover:bg-black/30' )} title={model?.favorite ? 'Favorit entfernen' : 'Als Favorit markieren'} aria-pressed={Boolean(model?.favorite)} @@ -1273,14 +1267,14 @@ export default function ModelDetails({ className={cn( 'absolute inset-0 size-4 transition-all duration-200 ease-out motion-reduce:transition-none', model?.favorite ? 'opacity-0 scale-75 rotate-12' : 'opacity-100 scale-100 rotate-0', - 'text-white/70' + 'text-gray-600 dark:text-white/70' )} /> @@ -1295,9 +1289,11 @@ export default function ModelDetails({ handleToggleLikeModel() }} className={cn( - 'inline-flex items-center justify-center rounded-full p-1.5 ring-1 ring-inset backdrop-blur', - 'cursor-pointer hover:scale-[1.03] active:scale-[0.98] transition', - model?.liked ? 'bg-rose-500/25 ring-rose-200/30' : 'bg-black/20 ring-white/15' + 'inline-flex items-center justify-center rounded-full p-1.5 ring-1 backdrop-blur shadow-sm', + 'cursor-pointer transition hover:scale-[1.03] active:scale-[0.98]', + model?.liked + ? 'bg-rose-100/95 text-rose-700 ring-rose-200 hover:bg-rose-200/95 dark:bg-rose-500/25 dark:text-rose-200 dark:ring-rose-200/30 dark:hover:bg-rose-500/30' + : 'bg-white/90 text-gray-700 ring-gray-200 hover:bg-white dark:bg-black/20 dark:text-white dark:ring-white/15 dark:hover:bg-black/30' )} title={model?.liked ? 'Like entfernen' : 'Liken'} aria-pressed={model?.liked === true} @@ -1308,14 +1304,14 @@ export default function ModelDetails({ className={cn( 'absolute inset-0 size-4 transition-all duration-200 ease-out motion-reduce:transition-none', model?.liked ? 'opacity-0 scale-75 rotate-12' : 'opacity-100 scale-100 rotate-0', - 'text-white/70' + 'text-gray-600 dark:text-white/70' )} /> diff --git a/frontend/src/components/ui/ModelPreview.tsx b/frontend/src/components/ui/ModelPreview.tsx index 4f210c2..a3eb998 100644 --- a/frontend/src/components/ui/ModelPreview.tsx +++ b/frontend/src/components/ui/ModelPreview.tsx @@ -27,7 +27,7 @@ type Props = { fastRetryMax?: number fastRetryWindowMs?: number - thumbsWebpUrl?: string | null + thumbsJPGUrl?: string | null thumbsCandidates?: Array } @@ -44,7 +44,7 @@ export default function ModelPreview({ fastRetryMs, fastRetryMax, fastRetryWindowMs, - thumbsWebpUrl, + thumbsJPGUrl, thumbsCandidates, }: Props) { const blurCls = blur ? 'blur-md' : '' @@ -92,7 +92,7 @@ export default function ModelPreview({ const thumbsCandidatesKey = useMemo(() => { const list = [ - thumbsWebpUrl, + thumbsJPGUrl, ...(Array.isArray(thumbsCandidates) ? thumbsCandidates : []), ] .map(normalizeUrl) @@ -100,7 +100,7 @@ export default function ModelPreview({ // Reihenfolge behalten, nur dedupe return Array.from(new Set(list)).join('|') - }, [thumbsWebpUrl, thumbsCandidates]) + }, [thumbsJPGUrl, thumbsCandidates]) // ✅ visibilitychange -> nur REF updaten useEffect(() => { @@ -383,7 +383,7 @@ export default function ModelPreview({ else setApiImgError(false) }} onError={() => { - // 1) Wenn direkte preview.webp fehlschlägt -> auf API-Fallback umschalten + // 1) Wenn direkte preview.jpg fehlschlägt -> auf API-Fallback umschalten if (useDirectThumb) { setDirectImgError(true) return diff --git a/frontend/src/components/ui/ModelsTab.tsx b/frontend/src/components/ui/ModelsTab.tsx index 9a1d7ad..2355848 100644 --- a/frontend/src/components/ui/ModelsTab.tsx +++ b/frontend/src/components/ui/ModelsTab.tsx @@ -1451,8 +1451,8 @@ export default function ModelsTab() { className={clsx( 'h-8 min-w-0 px-0 shadow-none', watch - ? 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-500/20 dark:text-indigo-200 dark:hover:bg-indigo-500/30' - : 'bg-white text-gray-700 ring-1 ring-gray-200 hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' + ? 'bg-indigo-50 text-indigo-700 ring-1 ring-indigo-200 shadow-xs hover:bg-indigo-100 dark:bg-indigo-500/20 dark:text-indigo-200 dark:ring-0 dark:shadow-none dark:hover:bg-indigo-500/30' + : 'bg-white text-gray-700 ring-1 ring-gray-200 shadow-none hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' )} title={watch ? 'Nicht mehr beobachten' : 'Beobachten'} onClick={(e) => { @@ -1473,8 +1473,8 @@ export default function ModelsTab() { className={clsx( 'h-8 min-w-0 px-0 shadow-none', fav - ? 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-500/20 dark:text-amber-200 dark:hover:bg-amber-500/30' - : 'bg-white text-gray-700 ring-1 ring-gray-200 hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' + ? 'bg-amber-50 text-amber-800 ring-1 ring-amber-200 shadow-xs hover:bg-amber-100 dark:bg-amber-500/20 dark:text-amber-200 dark:ring-0 dark:shadow-none dark:hover:bg-amber-500/30' + : 'bg-white text-gray-700 ring-1 ring-gray-200 shadow-none hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' )} title={fav ? 'Favorit entfernen' : 'Als Favorit markieren'} onClick={(e) => { @@ -1496,8 +1496,8 @@ export default function ModelsTab() { className={clsx( 'h-8 min-w-0 px-0 shadow-none', liked - ? 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-500/20 dark:text-rose-200 dark:hover:bg-rose-500/30' - : 'bg-white text-gray-700 ring-1 ring-gray-200 hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' + ? 'bg-rose-50 text-rose-700 ring-1 ring-rose-200 shadow-xs hover:bg-rose-100 dark:bg-rose-500/20 dark:text-rose-200 dark:ring-0 dark:shadow-none dark:hover:bg-rose-500/30' + : 'bg-white text-gray-700 ring-1 ring-gray-200 shadow-none hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' )} title={liked ? 'Gefällt mir entfernen' : 'Gefällt mir'} onClick={(e) => { @@ -1546,8 +1546,8 @@ export default function ModelsTab() { ? 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity' : 'opacity-100', watch - ? 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-500/20 dark:text-indigo-200 dark:hover:bg-indigo-500/30' - : 'bg-white text-gray-700 ring-1 ring-gray-200 hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' + ? 'bg-indigo-50 text-indigo-700 ring-1 ring-indigo-200 shadow-xs hover:bg-indigo-100 dark:bg-indigo-500/20 dark:text-indigo-200 dark:ring-0 dark:shadow-none dark:hover:bg-indigo-500/30' + : 'bg-white text-gray-700 ring-1 ring-gray-200 shadow-none hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' )} title={watch ? 'Nicht mehr beobachten' : 'Beobachten'} onClick={(e) => { @@ -1571,8 +1571,8 @@ export default function ModelsTab() { ? 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity' : 'opacity-100', fav - ? 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-500/20 dark:text-amber-200 dark:hover:bg-amber-500/30' - : 'bg-white text-gray-700 ring-1 ring-gray-200 hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' + ? 'bg-amber-50 text-amber-800 ring-1 ring-amber-200 shadow-xs hover:bg-amber-100 dark:bg-amber-500/20 dark:text-amber-200 dark:ring-0 dark:shadow-none dark:hover:bg-amber-500/30' + : 'bg-white text-gray-700 ring-1 ring-gray-200 shadow-none hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' )} title={fav ? 'Favorit entfernen' : 'Als Favorit markieren'} onClick={(e) => { @@ -1597,8 +1597,8 @@ export default function ModelsTab() { ? 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity' : 'opacity-100', liked - ? 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-500/20 dark:text-rose-200 dark:hover:bg-rose-500/30' - : 'bg-white text-gray-700 ring-1 ring-gray-200 hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' + ? 'bg-rose-50 text-rose-700 ring-1 ring-rose-200 shadow-xs hover:bg-rose-100 dark:bg-rose-500/20 dark:text-rose-200 dark:ring-0 dark:shadow-none dark:hover:bg-rose-500/30' + : 'bg-white text-gray-700 ring-1 ring-gray-200 shadow-none hover:bg-gray-100 dark:bg-white/5 dark:text-slate-200 dark:ring-0 dark:hover:bg-white/10' )} title={liked ? 'Gefällt mir entfernen' : 'Gefällt mir'} onClick={(e) => { diff --git a/frontend/src/components/ui/Pagination.tsx b/frontend/src/components/ui/Pagination.tsx index 74fa29c..2ae25d1 100644 --- a/frontend/src/components/ui/Pagination.tsx +++ b/frontend/src/components/ui/Pagination.tsx @@ -132,7 +132,7 @@ function PageButton({ roundedCls, disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer', active - ? 'z-10 bg-indigo-600 text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:focus-visible:outline-indigo-500' + ? 'z-10 !bg-indigo-600 !text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:focus-visible:outline-indigo-500' : 'text-gray-900 inset-ring inset-ring-gray-300 hover:bg-gray-50 dark:text-gray-200 dark:inset-ring-gray-700 dark:hover:bg-white/5' )} aria-current={active ? 'page' : undefined} diff --git a/frontend/src/components/ui/Player.tsx b/frontend/src/components/ui/Player.tsx index 37259e0..af50d83 100644 --- a/frontend/src/components/ui/Player.tsx +++ b/frontend/src/components/ui/Player.tsx @@ -416,7 +416,7 @@ export default function Player({ // Vorschaubild oben const previewA = React.useMemo( - () => apiUrl(`/api/preview?id=${encodeURIComponent(previewId)}&file=preview.webp`), + () => apiUrl(`/api/preview?id=${encodeURIComponent(previewId)}&file=preview.jpg`), [previewId] ) diff --git a/frontend/src/components/ui/RecorderSettings.tsx b/frontend/src/components/ui/RecorderSettings.tsx index f721aca..29e6013 100644 --- a/frontend/src/components/ui/RecorderSettings.tsx +++ b/frontend/src/components/ui/RecorderSettings.tsx @@ -534,35 +534,6 @@ export default function RecorderSettings({ onAssetsGenerated, onTaskRunningChang {/* Rechts: Alerts + Button */}
- {/* Alerts links neben Button */} - {saveUiState !== 'success' ? ( -
- {err ? ( -
- {err} -
- ) : msg ? ( -
- {msg} -
- ) : null} -
- ) : null} -
- +
) } diff --git a/frontend/src/components/ui/VideoSplitModal.tsx b/frontend/src/components/ui/VideoSplitModal.tsx index b72b127..8d22e2c 100644 --- a/frontend/src/components/ui/VideoSplitModal.tsx +++ b/frontend/src/components/ui/VideoSplitModal.tsx @@ -257,7 +257,7 @@ function previewIdFromJob(job: RecordJob | null): string { function previewStillSrcFromJob(job: RecordJob | null): string { const id = previewIdFromJob(job) if (!id) return '' - return `/api/preview?id=${encodeURIComponent(id)}&file=preview.webp` + return `/api/preview?id=${encodeURIComponent(id)}&file=preview.jpg` } function previewSpritesSrcFromJob(job: RecordJob | null): string { @@ -1042,7 +1042,7 @@ export default function VideoSplitModal({ }} title="Klicken zum Springen" > - {/* preview-sprites.webp als Hintergrund */} + {/* preview-sprites.jpg als Hintergrund */} {spriteMeta?.exists && spriteMeta.path && timelinePreviewTiles.length > 0 ? (