From 1da3c11f04d4ce0bcdc89aee1f1276eb2d3291a2 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Tue, 12 May 2026 09:25:27 +0200 Subject: [PATCH] bugfixes --- backend/.env | 2 +- backend/analyze.go | 2 +- backend/assets_generate.go | 33 +- backend/auth_passkey.go | 3 - backend/preview.go | 81 +- backend/rating.go | 41 +- backend/record_stream_cb.go | 12 +- backend/recorder_settings.json | 2 +- backend/tasks_regenerate_assets.go | 20 +- .../src/components/ui/FinishedDownloads.tsx | 86 +- frontend/src/components/ui/LoadingSpinner.tsx | 2 +- frontend/src/components/ui/RatingOverlay.tsx | 968 +++++++++++++----- 12 files changed, 907 insertions(+), 345 deletions(-) diff --git a/backend/.env b/backend/.env index e33764f..c1ed77b 100644 --- a/backend/.env +++ b/backend/.env @@ -1,2 +1,2 @@ -HTTPS_ENABLED=1 +HTTPS_ENABLED=0 AUTH_RP_ORIGINS=https://l14pbbk95100006.tegdssd.de:9999,https://l14pbbk95100006.tegdssd.de:5173,https://localhost:9999,https://127.0.0.1:9999,https://10.0.1.25:9999,http://localhost:5173,http://127.0.0.1:5173,http://10.0.1.25:5173 \ No newline at end of file diff --git a/backend/analyze.go b/backend/analyze.go index 75d1bfe..1afa952 100644 --- a/backend/analyze.go +++ b/backend/analyze.go @@ -980,7 +980,7 @@ func recordAnalyzeVideo(w http.ResponseWriter, r *http.Request) { segments = prepareAIRatingSegments(segments) appLogf("🧪 [analyze] rating segments file=%q count=%d", file, len(segments)) - rating := computeHighlightRating(segments, durationSec) + rating := computeHighlightRatingForVideo(segments, durationSec, outPath) ai := &aiAnalysisMeta{ Goal: "highlights", diff --git a/backend/assets_generate.go b/backend/assets_generate.go index d50803d..7e7ebf1 100644 --- a/backend/assets_generate.go +++ b/backend/assets_generate.go @@ -174,7 +174,7 @@ func ensureAnalyzeAllGoalsForVideoCtxForce( segments := buildAnalyzeSegmentsForGoal(hits, durationSec) segments = prepareAIRatingSegments(segments) - rating := computeHighlightRating(segments, durationSec) + rating := computeHighlightRatingForVideo(segments, durationSec, videoPath) ai := &aiAnalysisMeta{ Goal: "highlights", @@ -1375,24 +1375,35 @@ func hasAIAnalysisForOutputGoal(outPath string, goal string) bool { return false } - // Wichtig: - // Eine Analyse mit 0 Hits / 0 Segments ist trotzdem eine fertige Analyse. - // Sonst zeigt das Frontend fälschlich "Analyse fehlt". - if jsonNumberPositive(aiMap["analyzedAtUnix"]) { + // Eine Analyse ist erst dann fertig, wenn wirklich ein Rating-Objekt + // mit typischen Rating-Feldern gespeichert wurde. + rating, ok := aiMap["rating"].(map[string]any) + if !ok || rating == nil { + return false + } + + if jsonFieldExists(rating, "score") || + jsonFieldExists(rating, "stars") || + jsonFieldExists(rating, "segments") { return true } - if rating, ok := aiMap["rating"].(map[string]any); ok && rating != nil { - return true - } - - // Legacy-Fallback für alte meta.json-Dateien. + // Legacy-Fallback: alte Dateien ohne Rating, aber mit Hits/Segments. rawHits, hasHits := aiMap["hits"].([]any) rawSegs, hasSegs := aiMap["segments"].([]any) return (hasHits && len(rawHits) > 0) || (hasSegs && len(rawSegs) > 0) } +func jsonFieldExists(m map[string]any, key string) bool { + if m == nil { + return false + } + + _, ok := m[key] + return ok +} + func jsonNumberPositive(v any) bool { switch x := v.(type) { case json.Number: @@ -1522,7 +1533,7 @@ func prepareVideoForSplit(ctx context.Context, videoPath, sourceURL, goal string segments := buildAnalyzeSegmentsForGoal(hits, durationSec) segments = prepareAIRatingSegments(segments) - rating := computeHighlightRating(segments, durationSec) + rating := computeHighlightRatingForVideo(segments, durationSec, videoPath) ai := &aiAnalysisMeta{ Goal: "highlights", diff --git a/backend/auth_passkey.go b/backend/auth_passkey.go index d6ea50e..8e57467 100644 --- a/backend/auth_passkey.go +++ b/backend/auth_passkey.go @@ -81,9 +81,6 @@ func (am *AuthManager) initWebAuthn() error { return errors.New("no WebAuthn RP origins configured") } - appLogln("🔐 WebAuthn allowed origins:", strings.Join(origins, ", ")) - appLogln("🔐 AUTH_RP_ID:", strings.TrimSpace(os.Getenv("AUTH_RP_ID"))) - rpID, err := rpIDForOrigin(origins[0]) if err != nil { return err diff --git a/backend/preview.go b/backend/preview.go index e9305c3..c1bbdb9 100644 --- a/backend/preview.go +++ b/backend/preview.go @@ -1162,6 +1162,67 @@ func servePreviewStatusSVG(w http.ResponseWriter, label string, status int) { _, _ = w.Write([]byte(svg)) } +func shortFFmpegStderr(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + + lines := strings.Split(raw, "\n") + clean := make([]string, 0, len(lines)) + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + clean = append(clean, line) + } + + if len(clean) == 0 { + return "" + } + + want := []string{ + "Error while opening encoder", + "Could not open encoder", + "Invalid argument", + "Nothing was written into output file", + "Non full-range YUV", + "moov atom not found", + "Invalid data found", + "Error opening input", + } + + for _, needle := range want { + needleLower := strings.ToLower(needle) + + for _, line := range clean { + if strings.Contains(strings.ToLower(line), needleLower) { + if len(clean) > 1 { + return fmt.Sprintf("%s … (+%d Zeilen)", line, len(clean)-1) + } + return line + } + } + } + + if len(clean) == 1 { + return clean[0] + } + + return fmt.Sprintf("%s … (+%d Zeilen)", clean[len(clean)-1], len(clean)-1) +} + +func ffmpegPreviewError(label string, err error, stderr string) error { + msg := shortFFmpegStderr(stderr) + if msg == "" { + return appErrorf("%s: %w", label, err) + } + + return appErrorf("%s: %w: %s", label, err, msg) +} + // ============================================================ // JPG extraction + preview endpoint // Route: @@ -1189,6 +1250,8 @@ func extractLastFrameJPG(path string) ([]byte, error) { "-frames:v", "1", "-vf", "scale=720:-2:flags=fast_bilinear", "-vcodec", "mjpeg", + "-pix_fmt", "yuvj420p", + "-threads", "1", "-q:v", "4", "-f", "image2pipe", "pipe:1", @@ -1207,7 +1270,7 @@ func extractLastFrameJPG(path string) ([]byte, error) { if ctx.Err() == context.DeadlineExceeded { return nil, appErrorf("ffmpeg last-frame jpg: timeout") } - return nil, appErrorf("ffmpeg last-frame jpg: %w (%s)", err, strings.TrimSpace(stderr.String())) + return nil, ffmpegPreviewError("ffmpeg last-frame jpg", err, stderr.String()) } b := out.Bytes() @@ -1232,6 +1295,8 @@ func extractFrameAtTimeJPG(path string, seconds float64) ([]byte, error) { "-frames:v", "1", "-vf", "scale=720:-2", "-vcodec", "mjpeg", + "-pix_fmt", "yuvj420p", + "-threads", "1", "-q:v", "4", "-f", "image2pipe", "pipe:1", @@ -1246,7 +1311,7 @@ func extractFrameAtTimeJPG(path string, seconds float64) ([]byte, error) { cmd.Stdout = &out cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - return nil, appErrorf("ffmpeg frame-at-time jpg: %w (%s)", err, strings.TrimSpace(stderr.String())) + return nil, ffmpegPreviewError("ffmpeg frame-at-time jpg", err, stderr.String()) } b := out.Bytes() if len(b) == 0 { @@ -1280,6 +1345,8 @@ func extractLastFrameJPGScaled(path string, width int, quality int) ([]byte, err "-frames:v", "1", "-vf", fmt.Sprintf("scale=%d:-2", width), "-vcodec", "mjpeg", + "-pix_fmt", "yuvj420p", + "-threads", "1", "-q:v", qv, "-f", "image2pipe", "pipe:1", @@ -1294,7 +1361,7 @@ func extractLastFrameJPGScaled(path string, width int, quality int) ([]byte, err cmd.Stdout = &out cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - return nil, appErrorf("ffmpeg last-frame scaled jpg: %w (%s)", err, strings.TrimSpace(stderr.String())) + return nil, ffmpegPreviewError("ffmpeg last-frame scaled jpg", err, stderr.String()) } b := out.Bytes() if len(b) == 0 { @@ -1319,6 +1386,8 @@ func extractFirstFrameJPGScaled(path string, width int, quality int) ([]byte, er "-frames:v", "1", "-vf", fmt.Sprintf("scale=%d:-2", width), "-vcodec", "mjpeg", + "-pix_fmt", "yuvj420p", + "-threads", "1", "-q:v", "5", "-f", "image2pipe", "pipe:1", @@ -1333,7 +1402,7 @@ func extractFirstFrameJPGScaled(path string, width int, quality int) ([]byte, er cmd.Stdout = &out cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - return nil, appErrorf("ffmpeg first-frame scaled jpg: %w (%s)", err, strings.TrimSpace(stderr.String())) + return nil, ffmpegPreviewError("ffmpeg first-frame scaled jpg", err, stderr.String()) } b := out.Bytes() if len(b) == 0 { @@ -1370,6 +1439,8 @@ func extractLiveFrameFromM3U8ThumbJPG(ctx context.Context, m3u8URL, cookie, user "-frames:v", "1", "-vf", "scale=320:-2", "-vcodec", "mjpeg", + "-pix_fmt", "yuvj420p", + "-threads", "1", "-q:v", "6", "-f", "image2pipe", "pipe:1", @@ -1387,7 +1458,7 @@ func extractLiveFrameFromM3U8ThumbJPG(ctx context.Context, m3u8URL, cookie, user cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - return nil, appErrorf("ffmpeg live m3u8 jpg failed: %w (%s)", err, strings.TrimSpace(stderr.String())) + return nil, ffmpegPreviewError("ffmpeg live m3u8 jpg failed", err, stderr.String()) } b := out.Bytes() diff --git a/backend/rating.go b/backend/rating.go index edfbb65..b452720 100644 --- a/backend/rating.go +++ b/backend/rating.go @@ -1002,7 +1002,43 @@ func starsFromHighlightScore(score float64) int { } } +func ratingUsernameFromVideoPath(videoPath string) string { + id := strings.TrimSpace(assetIDFromVideoPath(videoPath)) + if id == "" { + return "" + } + + user := strings.ToLower(strings.TrimSpace(modelNameFromFilename(id))) + if user != "" && user != "unknown" { + return user + } + + // Fallback nur, wenn die ID nicht wie ein kompletter Dateiname mit Timestamp aussieht. + if !strings.Contains(id, "__") { + return strings.ToLower(strings.TrimSpace(id)) + } + + return "" +} + +func ratingLogSubject(username string) string { + username = strings.ToLower(strings.TrimSpace(username)) + if username == "" || username == "unknown" { + return "[rating]" + } + + return "[" + username + "]" +} + func computeHighlightRating(segments []aiSegmentMeta, durationSec float64) *aiRatingMeta { + return computeHighlightRatingWithUsername(segments, durationSec, "") +} + +func computeHighlightRatingForVideo(segments []aiSegmentMeta, durationSec float64, videoPath string) *aiRatingMeta { + return computeHighlightRatingWithUsername(segments, durationSec, ratingUsernameFromVideoPath(videoPath)) +} + +func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec float64, username string) *aiRatingMeta { r := &aiRatingMeta{ Score: 0, Stars: 1, @@ -1079,7 +1115,7 @@ func computeHighlightRating(segments []aiSegmentMeta, durationSec float64) *aiRa } if n == 0 { - appLogln("⚠️ [rating] result is zero because all segments were skipped") + appLogf("⚠️ %s rating result is zero because all segments were skipped", ratingLogSubject(username)) return r } @@ -1132,7 +1168,8 @@ func computeHighlightRating(segments []aiSegmentMeta, durationSec float64) *aiRa r.AvgConfidence = ratingRound(avgConfidence, 3) appLogf( - "✅ [rating] result score=%.1f stars=%d segments=%d flagged=%.2f weighted=%.2f coverage=%.4f weightedCoverage=%.4f longest=%.2f avgConf=%.3f", + "✅ %s rating result score=%.1f stars=%d segments=%d flagged=%.2f weighted=%.2f coverage=%.4f weightedCoverage=%.4f longest=%.2f avgConf=%.3f", + ratingLogSubject(username), r.Score, r.Stars, r.Segments, diff --git a/backend/record_stream_cb.go b/backend/record_stream_cb.go index 3730719..8bb3930 100644 --- a/backend/record_stream_cb.go +++ b/backend/record_stream_cb.go @@ -42,17 +42,17 @@ func RecordStream( loadFreshHLS := func() (*selectedHLSStream, error) { body, err := hc.FetchPage(ctx, pageURL, httpCookie) if err != nil { - return nil, appErrorf("seite laden: %w", err) + return nil, appErrorf("[%s] seite laden: %w", username, err) } hlsURL, err := ParseStream(body) if err != nil { - return nil, appErrorf("stream-parsing: %w", err) + return nil, appErrorf("[%s] stream-parsing: %w", username, err) } hlsURL = strings.TrimSpace(hlsURL) if hlsURL == "" { - return nil, errors.New("leere hls url") + return nil, appErrorf("[%s] leere hls url", username) } stream, err := getWantedResolutionPlaylistWithHeaders( @@ -63,10 +63,10 @@ func RecordStream( pageURL, ) if err != nil { - return nil, appErrorf("variant-playlist: %w", err) + return nil, appErrorf("[%s] variant-playlist: %w", username, err) } if stream == nil || strings.TrimSpace(stream.VideoURL) == "" { - return nil, errors.New("leere final video url") + return nil, appErrorf("[%s] leere final video url", username) } return stream, nil @@ -257,8 +257,6 @@ func RecordStream( newStream, rerr := loadFreshHLSWithRetry(3, "after-ffmpeg-error") if rerr != nil { - appLogln("⚠️ chaturbate hls refresh failed:", rerr) - select { case <-ctx.Done(): return ctx.Err() diff --git a/backend/recorder_settings.json b/backend/recorder_settings.json index c602d34..0e71787 100644 --- a/backend/recorder_settings.json +++ b/backend/recorder_settings.json @@ -11,7 +11,7 @@ "useMyFreeCamsWatcher": true, "autoDeleteSmallDownloads": true, "autoDeleteSmallDownloadsBelowMB": 300, - "autoDeleteSmallDownloadsKeepFavorites": false, + "autoDeleteSmallDownloadsKeepFavorites": true, "lowDiskPauseBelowGB": 5, "blurPreviews": false, "teaserPlayback": "all", diff --git a/backend/tasks_regenerate_assets.go b/backend/tasks_regenerate_assets.go index 78f31f1..2fbff31 100644 --- a/backend/tasks_regenerate_assets.go +++ b/backend/tasks_regenerate_assets.go @@ -125,7 +125,7 @@ func regeneratePhaseTruthForVideo(videoPath string, assetID string, goal string) } // KI nur dann fertig, wenn Sprite existiert UND passende AI in meta.json vorhanden ist - out.AnalyzeReady = out.SpritesReady && hasAIAnalysisForOutputGoal(videoPath, goal) + out.AnalyzeReady = hasAIAnalysisForOutputGoal(videoPath, goal) } return out @@ -149,12 +149,10 @@ func firstMissingPrimaryRegeneratePhaseError(videoPath string, assetID string) e func firstMissingDeferredRegeneratePhaseError(videoPath string, assetID string) error { truth := regeneratePhaseTruthForVideo(videoPath, assetID, "highlights") - if !truth.SpritesReady { - return errors.New("preview-sprite.jpg fehlt oder ist leer") - } if !truth.AnalyzeReady { return errors.New("Analyse fehlt oder wurde nicht in meta.json gespeichert") } + return nil } @@ -454,8 +452,18 @@ func runRegenerateAssetsJob(job *regenerateAssetsJob) { } if !regeneratePhaseTruthForVideo(videoPath, id, primaryGoal).SpritesReady { - failJob("postwork", "sprites", "preview-sprite.jpg fehlt oder ist leer") - return + // Sprite ist nice-to-have. Analyse/Rating läuft frame-basiert und darf weiterlaufen. + publishFinishedPostworkPhase( + file, + id, + "postwork", + "sprites", + "error", + "Sprites fehlgeschlagen", + "preview-sprite.jpg fehlt oder ist leer", + ) + } else { + publishFinishedPostworkPhase(file, id, "postwork", "sprites", "done", "Sprites", "") } publishFinishedPostworkPhase(file, id, "postwork", "sprites", "done", "Sprites", "") diff --git a/frontend/src/components/ui/FinishedDownloads.tsx b/frontend/src/components/ui/FinishedDownloads.tsx index e4bb021..f044c0b 100644 --- a/frontend/src/components/ui/FinishedDownloads.tsx +++ b/frontend/src/components/ui/FinishedDownloads.tsx @@ -1616,14 +1616,27 @@ function postworkDoneCount(summary: FinishedPostworkSummary): number { return summary.steps.filter((step) => step.state === 'done').length } +function postworkStepProgressValue(step: FinishedPostworkStepSummary): number { + if (step.state === 'done') return 1 + + if (step.state === 'running' && isAnalyzeStep(step)) { + const percent = analysisProgressPercent(step.label) + if (percent != null) return percent / 100 + } + + return 0 +} + function postworkProgressPercent(summary: FinishedPostworkSummary): number { const total = summary.steps.length if (total <= 0) return 0 - const done = postworkDoneCount(summary) - const running = summary.steps.some((step) => step.state === 'running') ? 0.5 : 0 + const value = summary.steps.reduce( + (sum, step) => sum + postworkStepProgressValue(step), + 0 + ) - return Math.max(0, Math.min(100, Math.round(((done + running) / total) * 100))) + return Math.max(0, Math.min(100, Math.round((value / total) * 100))) } function postworkStepStateLabel(step: FinishedPostworkStepSummary): string { @@ -1677,28 +1690,55 @@ function postworkStepIcon(step: FinishedPostworkStepSummary): ReactNode { return } +function postworkPreviewSrc(summary: FinishedPostworkSummary): string | null { + const id = String(summary.assetId || fileStemFromOutput(summary.file)).trim() + if (!id) return null + + const base = `/api/preview?id=${encodeURIComponent(id)}&file=preview.jpg` + const version = Number.isFinite(summary.ts) && summary.ts > 0 ? String(summary.ts) : '' + + return version ? `${base}&v=${encodeURIComponent(version)}` : base +} + function renderPostworkPopover(summary: FinishedPostworkSummary): ReactNode { const done = postworkDoneCount(summary) const total = summary.steps.length const progress = postworkProgressPercent(summary) + const previewSrc = postworkPreviewSrc(summary) return ( -
-
-
-
+
+
+ {previewSrc ? ( + { + e.currentTarget.style.display = 'none' + }} + /> + ) : null} + +
+
+ +
+
{postworkSummaryIcon(summary)}
-
+
{postworkSummaryTitle(summary)}
@@ -1706,47 +1746,47 @@ function renderPostworkPopover(summary: FinishedPostworkSummary): ReactNode {
-
+
{summary.file}
-
-
+
+
- + {done}/{total}
-
-
+
+
{summary.steps.map((step) => (
-
+
{postworkStepDisplayLabel(step)}
@@ -1757,14 +1797,14 @@ function renderPostworkPopover(summary: FinishedPostworkSummary): ReactNode { ) : null}
-
+
{postworkStepIcon(step)}