diff --git a/backend/assets/img/cb.jpg b/backend/assets/img/cb.jpg new file mode 100644 index 0000000..23fbc8b Binary files /dev/null and b/backend/assets/img/cb.jpg differ diff --git a/backend/assets/img/mfc.jpg b/backend/assets/img/mfc.jpg new file mode 100644 index 0000000..e792955 Binary files /dev/null and b/backend/assets/img/mfc.jpg differ diff --git a/backend/preview.go b/backend/preview.go index 9c15574..91ca494 100644 --- a/backend/preview.go +++ b/backend/preview.go @@ -6,6 +6,7 @@ import ( "bytes" "context" "crypto/sha1" + "embed" "encoding/hex" "encoding/json" "errors" @@ -58,6 +59,9 @@ func setCoverModelStore(s *ModelStore) { coverModelStore = s } var errCoverNotSupported = errors.New("cover not supported") +//go:embed assets/img/cb.jpg assets/img/mfc.jpg +var embeddedFallbackImages embed.FS + // ============================================================ // Covers: generated/covers/. + info.json // Routes: @@ -1501,6 +1505,99 @@ func serveLivePreviewJPGBytes(w http.ResponseWriter, b []byte) { _, _ = w.Write(b) } +func detectPreviewProvider(job *RecordJob, outPath string) string { + if job != nil { + sourceURL := strings.ToLower(strings.TrimSpace(job.SourceURL)) + + switch { + case strings.Contains(sourceURL, "chaturbate.com"): + return "chaturbate" + case strings.Contains(sourceURL, "myfreecams.com"): + return "mfc" + } + } + + modelKey := strings.TrimSpace(inferModelFromThumbLike(outPath)) + if modelKey == "" || coverModelStore == nil { + return "" + } + modelKey = strings.ToLower(modelKey) + + models := coverModelStore.List() + for _, m := range models { + if strings.ToLower(strings.TrimSpace(m.ModelKey)) != modelKey { + continue + } + + host := strings.ToLower(strings.TrimSpace(m.Host)) + input := strings.ToLower(strings.TrimSpace(m.Input)) + + switch { + case strings.Contains(host, "chaturbate"), strings.Contains(input, "chaturbate.com"): + return "chaturbate" + case strings.Contains(host, "myfreecams"), strings.Contains(input, "myfreecams.com"): + return "mfc" + } + } + + return "" +} + +func fallbackPreviewImageAssetPathForProvider(provider string) (string, error) { + switch strings.ToLower(strings.TrimSpace(provider)) { + case "chaturbate": + return "assets/img/cb.jpg", nil + case "mfc": + return "assets/img/mfc.jpg", nil + default: + return "", fmt.Errorf("unknown provider") + } +} + +func serveFallbackPreviewImage(w http.ResponseWriter, r *http.Request, job *RecordJob, outPath string) bool { + provider := detectPreviewProvider(job, outPath) + if provider == "" { + return false + } + + assetPath, err := fallbackPreviewImageAssetPathForProvider(provider) + if err != nil { + return false + } + + b, err := embeddedFallbackImages.ReadFile(assetPath) + if err != nil || len(b) == 0 { + return false + } + + ext := strings.ToLower(filepath.Ext(assetPath)) + switch ext { + case ".png": + w.Header().Set("Content-Type", "image/png") + default: + w.Header().Set("Content-Type", "image/jpeg") + } + + w.Header().Set("Cache-Control", "public, max-age=60") + w.Header().Set("X-Content-Type-Options", "nosniff") + + if r.Method == http.MethodHead { + w.WriteHeader(http.StatusOK) + return true + } + + w.WriteHeader(http.StatusOK) + _, _ = w.Write(b) + return true +} + +func serveProviderFallbackOrStatus(w http.ResponseWriter, r *http.Request, job *RecordJob, outPath string, label string, status int) { + if serveFallbackPreviewImage(w, r, job, outPath) { + return + } + servePreviewStatusSVG(w, label, status) +} + func servePreviewJPGAlias(w http.ResponseWriter, r *http.Request, id string) { jobsMu.Lock() job := jobs[id] @@ -1531,7 +1628,7 @@ func servePreviewJPGAlias(w http.ResponseWriter, r *http.Request, id string) { } } - servePreviewStatusSVG(w, "Preview", http.StatusOK) + serveProviderFallbackOrStatus(w, r, job, job.Output, "Preview", http.StatusOK) return } @@ -1649,15 +1746,15 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri jobsMu.Unlock() if state == "private" { - servePreviewStatusSVG(w, "Private", http.StatusOK) + serveProviderFallbackOrStatus(w, r, job, job.Output, "Private", http.StatusOK) return } if state == "offline" { - servePreviewStatusSVG(w, "Offline", http.StatusOK) + serveProviderFallbackOrStatus(w, r, job, job.Output, "Offline", http.StatusOK) return } - servePreviewStatusSVG(w, "Preview", http.StatusOK) + serveProviderFallbackOrStatus(w, r, job, job.Output, "Preview", http.StatusOK) return } @@ -1792,6 +1889,11 @@ func servePreviewForFinishedFile(w http.ResponseWriter, r *http.Request, id stri return } + // Wenn noch kein generiertes preview.jpg existiert, Provider-Fallbackbild liefern + if serveFallbackPreviewImage(w, r, nil, outPath) { + return + } + genCtx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() diff --git a/frontend/src/components/ui/FinishedDownloads.tsx b/frontend/src/components/ui/FinishedDownloads.tsx index 0f146de..da642fd 100644 --- a/frontend/src/components/ui/FinishedDownloads.tsx +++ b/frontend/src/components/ui/FinishedDownloads.tsx @@ -28,7 +28,7 @@ import { AdjustmentsHorizontalIcon, CheckCircleIcon, ExclamationTriangleIcon, - SparklesIcon, + ClockIcon, CheckIcon, XMarkIcon, ArchiveBoxIcon, @@ -1146,7 +1146,7 @@ export function renderFinishedPostworkBadge( {summary.state === 'running' ? ( ) : summary.state === 'queued' ? ( - + ) : summary.state === 'done' ? ( ) : summary.state === 'error' ? ( @@ -1162,7 +1162,7 @@ export function renderFinishedPostworkBadge( {summary.state === 'running' ? ( ) : summary.state === 'queued' ? ( - + ) : summary.state === 'done' ? ( ) : summary.state === 'error' ? ( @@ -1208,7 +1208,7 @@ export function renderFinishedPostworkBadge( ) : step.state === 'queued' ? ( <> - + {typeof step.position === 'number' && step.position > 0 ? `Wartet #${step.position}` diff --git a/frontend/src/components/ui/HoverPopover.tsx b/frontend/src/components/ui/HoverPopover.tsx index a177269..3f9eefe 100644 --- a/frontend/src/components/ui/HoverPopover.tsx +++ b/frontend/src/components/ui/HoverPopover.tsx @@ -142,7 +142,7 @@ export default function HoverPopover({ children, content }: HoverPopoverProps) { createPortal(