From ae957af7b2e1b53150146edf3f05e6f4afd9bd87 Mon Sep 17 00:00:00 2001
From: Linrador <68631622+Linrador@users.noreply.github.com>
Date: Mon, 1 Jun 2026 09:16:21 +0200
Subject: [PATCH] bugfixes and optimizations
---
backend/assets_generate.go | 2 +-
backend/postwork.go | 52 +++++
backend/preview.go | 6 -
backend/recorder_settings.json | 1 +
backend/settings.go | 19 ++
backend/startup_leftovers.go | 179 +++++++++++++++++-
backend/tasks_cleanup.go | 101 +++++++---
frontend/src/App.tsx | 128 +++++++++----
frontend/src/components/ui/Downloads.tsx | 2 +-
.../src/components/ui/FinishedDownloads.tsx | 14 +-
.../ui/FinishedDownloadsCardsView.tsx | 13 +-
.../ui/FinishedDownloadsDesktopCard.tsx | 113 +++++++----
.../ui/FinishedDownloadsGalleryCard.tsx | 57 +++++-
.../src/components/ui/PerformanceMonitor.tsx | 10 +-
.../src/components/ui/RecorderSettings.tsx | 18 ++
frontend/src/components/ui/Tabs.tsx | 29 ++-
frontend/src/components/ui/TrainingTab.tsx | 2 +-
frontend/src/index.css | 31 +++
frontend/src/types.ts | 12 ++
19 files changed, 626 insertions(+), 163 deletions(-)
diff --git a/backend/assets_generate.go b/backend/assets_generate.go
index 15d78b8..1110eb2 100644
--- a/backend/assets_generate.go
+++ b/backend/assets_generate.go
@@ -992,7 +992,7 @@ func enqueueDeferredAssetsAndAI(job *RecordJob, outPath string, sourceURL string
sortBucket, sortName := postWorkSortForVideoPath(outPath)
- ok := enrichQ.Enqueue(PostWorkTask{
+ ok := enqueueEnrichPostwork(PostWorkTask{
Key: "enrich:" + assetID,
Added: time.Now(),
SortBucket: sortBucket,
diff --git a/backend/postwork.go b/backend/postwork.go
index a06dc2c..7643508 100644
--- a/backend/postwork.go
+++ b/backend/postwork.go
@@ -252,6 +252,45 @@ func (pq *PostWorkQueue) RemoveQueued(key string) bool {
return true
}
+func (pq *PostWorkQueue) CancelAll() (queuedRemoved int, runningCancelled int) {
+ pq.mu.Lock()
+
+ waiting := append([]string(nil), pq.waitingKeys...)
+ runningCancels := make([]context.CancelFunc, 0, len(pq.cancelByKey))
+
+ for key, cancel := range pq.cancelByKey {
+ if _, running := pq.runningKeys[key]; running && cancel != nil {
+ runningCancels = append(runningCancels, cancel)
+ }
+ }
+
+ for _, key := range waiting {
+ delete(pq.inflight, key)
+ pq.unlockFileLocked(key)
+
+ if pq.queued > 0 {
+ pq.queued--
+ }
+
+ queuedRemoved++
+ }
+
+ pq.waitingKeys = nil
+ pq.queue = nil
+
+ runningCancelled = len(runningCancels)
+
+ pq.mu.Unlock()
+
+ for _, cancel := range runningCancels {
+ cancel()
+ }
+
+ publishTaskState()
+
+ return queuedRemoved, runningCancelled
+}
+
func (pq *PostWorkQueue) workerLoop(id int) {
for {
pq.mu.Lock()
@@ -393,6 +432,19 @@ func (pq *PostWorkQueue) StatusForKey(key string) PostWorkKeyStatus {
var postWorkQ = NewPostWorkQueue(1024, 1) // schneller kritischer Pfad
var enrichQ = NewPostWorkQueue(128, 1) // langsame Hintergrundaufgaben
+func enrichPostworkEnabled() bool {
+ return getSettings().EnrichPostworkEnabled
+}
+
+func enqueueEnrichPostwork(task PostWorkTask) bool {
+ if !enrichPostworkEnabled() {
+ appLogln("⏭️ enrich postwork deaktiviert, überspringe:", task.Key)
+ return false
+ }
+
+ return enrichQ.Enqueue(task)
+}
+
// --- Status Refresher (ehemals postwork_refresh.go) ---
type enrichStatusSnapshot struct {
diff --git a/backend/preview.go b/backend/preview.go
index c1bbdb9..5d2ce22 100644
--- a/backend/preview.go
+++ b/backend/preview.go
@@ -1274,9 +1274,6 @@ func extractLastFrameJPG(path string) ([]byte, error) {
}
b := out.Bytes()
- if len(b) == 0 {
- return nil, appErrorf("ffmpeg last-frame jpg: empty output")
- }
return b, nil
}
@@ -1314,9 +1311,6 @@ func extractFrameAtTimeJPG(path string, seconds float64) ([]byte, error) {
return nil, ffmpegPreviewError("ffmpeg frame-at-time jpg", err, stderr.String())
}
b := out.Bytes()
- if len(b) == 0 {
- return nil, appErrorf("ffmpeg frame-at-time jpg: empty output")
- }
return b, nil
}
diff --git a/backend/recorder_settings.json b/backend/recorder_settings.json
index c602d34..a6b46aa 100644
--- a/backend/recorder_settings.json
+++ b/backend/recorder_settings.json
@@ -22,6 +22,7 @@
"generateAssetsTeaser": true,
"generateAssetsSprites": false,
"generateAssetsAnalyze": false,
+ "enrichPostworkEnabled": false,
"trainingRecognitionEnabled": true,
"trainingDetectorEpochs": 60,
"encryptedCookies": "FEPj/igIPaDy8xr709QjHKOqXWVGesegmwzhTP1Whnusetrl1WenN7t0V1Rm/lw2nLkyis78kz+4yjsDtVgO3MXLU4Uq85AhAJQOE7SMVD+YMNenCS8Qr0JLkmI6cr/kR6sKrx7Jm6x6DY1BolFFMX1Ii3EO+8b4Re2GYaFi1iuh97oI8Zg4r5sMYmt7FIMRkr1uy0u0ToH8hsO0iDoExcxubQNHIZTQ07kkWP70Up78tYs59INxzosijATbiVhOT1H0YwS23iWC3bwLgeo2lOzMejJWpEG16CN55CoEAymFd2ktlQZ4Lmv0FlnBJ48H27Cj0TKR1yRbBxpGjgch9x0421C5qYGeXNB9jgczEKh28oYdt8ljXejoDsV7/oKYNaAFfIlICsC/Zz97ZpPRrKiRTSTkTLmIXjnWtVdwGRfCvJrYjJpoe3ZOaubulqCQs6Ug501b6JTzdZujKAl68tL5stZD7hH9XGQSpquZWGeSUGOrI5jU8dULbxCLpkGtiOEmxGb93OLYdHQ8JJg6zHeExVtPSqANHoKX3NpFc33fqOX1vOQVeg8Ei4ymxntIMOwQWs8xnbuKXmnNx6GTBSqHiD7X/a8oeEkMSw1+bCODvwuXlNBcdxHlf3/wkvHb9lhnh231BD1+Ufrnij65Apko8A=="
diff --git a/backend/settings.go b/backend/settings.go
index daa7b63..870797b 100644
--- a/backend/settings.go
+++ b/backend/settings.go
@@ -44,6 +44,7 @@ type RecorderSettings struct {
GenerateAssetsTeaser bool `json:"generateAssetsTeaser"`
GenerateAssetsSprites bool `json:"generateAssetsSprites"`
GenerateAssetsAnalyze bool `json:"generateAssetsAnalyze"`
+ EnrichPostworkEnabled bool `json:"enrichPostworkEnabled"`
TrainingRecognitionEnabled bool `json:"trainingRecognitionEnabled"`
TrainingDetectorEpochs int `json:"trainingDetectorEpochs"`
@@ -84,6 +85,7 @@ var (
GenerateAssetsTeaser: true,
GenerateAssetsSprites: false,
GenerateAssetsAnalyze: false,
+ EnrichPostworkEnabled: true,
TrainingRecognitionEnabled: true,
TrainingDetectorEpochs: 60,
@@ -256,6 +258,7 @@ type RecorderSettingsPublic struct {
GenerateAssetsTeaser bool `json:"generateAssetsTeaser"`
GenerateAssetsSprites bool `json:"generateAssetsSprites"`
GenerateAssetsAnalyze bool `json:"generateAssetsAnalyze"`
+ EnrichPostworkEnabled bool `json:"enrichPostworkEnabled"`
TrainingRecognitionEnabled bool `json:"trainingRecognitionEnabled"`
TrainingDetectorEpochs int `json:"trainingDetectorEpochs"`
@@ -293,6 +296,7 @@ func toPublicSettings(s RecorderSettings) RecorderSettingsPublic {
GenerateAssetsTeaser: s.GenerateAssetsTeaser,
GenerateAssetsSprites: s.GenerateAssetsSprites,
GenerateAssetsAnalyze: s.GenerateAssetsAnalyze,
+ EnrichPostworkEnabled: s.EnrichPostworkEnabled,
TrainingRecognitionEnabled: s.TrainingRecognitionEnabled,
TrainingDetectorEpochs: s.TrainingDetectorEpochs,
@@ -455,11 +459,14 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) {
next.EnableNotifications = in.EnableNotifications
+ oldEnrichPostworkEnabled := current.EnrichPostworkEnabled
+
next.GenerateAssetsMeta = in.GenerateAssetsMeta
next.GenerateAssetsThumb = in.GenerateAssetsThumb
next.GenerateAssetsTeaser = in.GenerateAssetsTeaser
next.GenerateAssetsSprites = in.GenerateAssetsSprites
next.GenerateAssetsAnalyze = in.GenerateAssetsAnalyze
+ next.EnrichPostworkEnabled = in.EnrichPostworkEnabled
next.TrainingRecognitionEnabled = in.TrainingRecognitionEnabled
next.TrainingDetectorEpochs = in.TrainingDetectorEpochs
@@ -492,6 +499,18 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) {
saveSettingsToDisk()
+ if oldEnrichPostworkEnabled && !next.EnrichPostworkEnabled {
+ cancelledQueued, cancelledRunning := enrichQ.CancelAll()
+
+ if cancelledQueued > 0 || cancelledRunning > 0 {
+ appLogln(
+ "🛑 enrich postwork deaktiviert:",
+ "queuedRemoved=", cancelledQueued,
+ "runningCancelled=", cancelledRunning,
+ )
+ }
+ }
+
if newStore != nil {
setModelStore(newStore)
setChaturbateOnlineModelStore(newStore)
diff --git a/backend/startup_leftovers.go b/backend/startup_leftovers.go
index 35e72a7..d3c602e 100644
--- a/backend/startup_leftovers.go
+++ b/backend/startup_leftovers.go
@@ -11,24 +11,185 @@ import (
"github.com/google/uuid"
)
-const startupLeftoverMinAge = 60 * time.Second
+const (
+ startupLeftoverMinAge = 60 * time.Second
+
+ // .muxing.ts sind temporäre Zwischenstände.
+ // Nur alte Dateien löschen, damit ein sehr frischer/noch schreibender Prozess
+ // nicht versehentlich getroffen wird.
+ startupMuxingTempMinAge = 10 * time.Minute
+
+ postworkLeftoverInitialDelay = 8 * time.Second
+ postworkLeftoverScanInterval = 1 * time.Hour
+)
func startPostworkLeftoverScanOnStartup() {
go func() {
// Kurz warten, bis Settings, Queues und UI/SSE bereit sind.
- time.Sleep(8 * time.Second)
+ time.Sleep(postworkLeftoverInitialDelay)
- queued, skipped := enqueuePostworkLeftoversFromRecordDir()
- if queued > 0 || skipped > 0 {
- appLogln(
- "🧩 [startup-postwork] leftover scan fertig:",
- "queued=", queued,
- "skipped=", skipped,
- )
+ runPostworkLeftoverScan := func(reason string) {
+ removedMuxing, skippedMuxing := cleanupStaleMuxingTempFilesFromRecordDir()
+
+ queued, skipped := enqueuePostworkLeftoversFromRecordDir()
+ if queued > 0 || skipped > 0 || removedMuxing > 0 || skippedMuxing > 0 {
+ appLogln(
+ "🧩 [startup-postwork] leftover scan fertig:",
+ "reason=", reason,
+ "queued=", queued,
+ "skipped=", skipped,
+ "removedMuxing=", removedMuxing,
+ "skippedMuxing=", skippedMuxing,
+ )
+ }
+ }
+
+ runPostworkLeftoverScan("startup")
+
+ ticker := time.NewTicker(postworkLeftoverScanInterval)
+ defer ticker.Stop()
+
+ for range ticker.C {
+ runPostworkLeftoverScan("interval")
}
}()
}
+func cleanupStaleMuxingTempFilesFromRecordDir() (removed int, skipped int) {
+ s := getSettings()
+
+ recordAbs, err := resolvePathRelativeToApp(s.RecordDir)
+ if err != nil || strings.TrimSpace(recordAbs) == "" {
+ recordAbs = strings.TrimSpace(s.RecordDir)
+ }
+
+ recordAbs = strings.TrimSpace(recordAbs)
+ if recordAbs == "" {
+ appLogln("⚠️ [startup-postwork] recordDir ist leer")
+ return 0, 0
+ }
+
+ entries, err := os.ReadDir(recordAbs)
+ if err != nil {
+ appLogln("⚠️ [startup-postwork] recordDir lesen fehlgeschlagen:", err)
+ return 0, 0
+ }
+
+ now := time.Now()
+
+ for _, e := range entries {
+ if e == nil || e.IsDir() {
+ continue
+ }
+
+ name := strings.TrimSpace(e.Name())
+ if name == "" {
+ continue
+ }
+
+ if !isMuxingTempTSName(name) {
+ continue
+ }
+
+ path := filepath.Join(recordAbs, name)
+
+ fi, err := os.Stat(path)
+ if err != nil || fi == nil || fi.IsDir() {
+ skipped++
+ continue
+ }
+
+ if fi.Size() <= 0 {
+ if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
+ skipped++
+ appLogln("⚠️ [startup-postwork] .muxing.ts leer, aber löschen fehlgeschlagen:", name, err)
+ continue
+ }
+
+ removed++
+ appLogln("🗑️ [startup-postwork] leere .muxing.ts gelöscht:", name)
+ continue
+ }
+
+ age := now.Sub(fi.ModTime())
+ if age < startupMuxingTempMinAge {
+ skipped++
+ appLogln(
+ "⏭️ [startup-postwork] .muxing.ts zu frisch:",
+ name,
+ "age=", age.Round(time.Second),
+ "min=", startupMuxingTempMinAge,
+ )
+ continue
+ }
+
+ if !singleFileStable(path) {
+ skipped++
+ appLogln("⏭️ [startup-postwork] .muxing.ts nicht stabil:", name)
+ continue
+ }
+
+ // Falls für diesen temporären Namen ein Audio-Sidecar existiert, direkt mit entfernen.
+ removedAudioSidecars := removePostworkAudioSidecarsForOutput(path)
+
+ if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
+ skipped++
+ appLogln("⚠️ [startup-postwork] .muxing.ts löschen fehlgeschlagen:", name, err)
+ continue
+ }
+
+ removed++
+ appLogln(
+ "🗑️ [startup-postwork] alte .muxing.ts gelöscht:",
+ name,
+ "audioSidecars=", len(removedAudioSidecars),
+ )
+ }
+
+ return removed, skipped
+}
+
+func isMuxingTempTSName(name string) bool {
+ name = strings.TrimSpace(filepath.Base(name))
+ if name == "" {
+ return false
+ }
+
+ lowerName := strings.ToLower(name)
+
+ return strings.HasSuffix(lowerName, ".muxing.ts")
+}
+
+func singleFileStable(path string) bool {
+ path = strings.TrimSpace(path)
+ if path == "" {
+ return false
+ }
+
+ sizeOf := func(p string) (int64, bool) {
+ fi, err := os.Stat(p)
+ if err != nil || fi == nil || fi.IsDir() {
+ return 0, false
+ }
+
+ return fi.Size(), true
+ }
+
+ size1, ok1 := sizeOf(path)
+ if !ok1 {
+ return false
+ }
+
+ time.Sleep(1500 * time.Millisecond)
+
+ size2, ok2 := sizeOf(path)
+ if !ok2 {
+ return false
+ }
+
+ return size1 == size2
+}
+
func enqueuePostworkLeftoversFromRecordDir() (queued int, skipped int) {
s := getSettings()
diff --git a/backend/tasks_cleanup.go b/backend/tasks_cleanup.go
index 0f375ad..b621e72 100644
--- a/backend/tasks_cleanup.go
+++ b/backend/tasks_cleanup.go
@@ -33,7 +33,12 @@ type cleanupResp struct {
DeletedBytesHuman string `json:"deletedBytesHuman"`
DeletedRecordOrphans int `json:"deletedRecordOrphans"`
DeletedRecordOrphanBytes int64 `json:"deletedRecordOrphanBytes"`
- ErrorCount int `json:"errorCount"`
+
+ DeletedMuxingTemps int `json:"deletedMuxingTemps"`
+ DeletedAudioOrphans int `json:"deletedAudioOrphans"`
+ DeletedTSOrphans int `json:"deletedTSOrphans"`
+
+ ErrorCount int `json:"errorCount"`
// Generated-GC
GeneratedOrphansChecked int `json:"generatedOrphansChecked"`
@@ -246,7 +251,7 @@ func settingsCleanupHandler(w http.ResponseWriter, r *http.Request) {
finishCleanupJob(
jobID,
- cleanupProgressText(resp.DeletedFiles, resp.DeletedBrokenFiles, resp.ScannedFiles, resp.DeletedBytes),
+ cleanupProgressText(resp),
nil,
)
}(jobID, ctx, doneAbs, recordAbs, mb)
@@ -278,23 +283,42 @@ func settingsCleanupHandler(w http.ResponseWriter, r *http.Request) {
}
}
-func cleanupProgressText(deleted int, broken int, total int, freedBytes int64) string {
- if broken > 0 {
- return fmt.Sprintf(
- "gelöscht: %d/%d · fehlerhaft: %d · freigegeben: %s",
- deleted,
- total,
- broken,
- formatBytesSI(freedBytes),
- )
+func cleanupProgressText(resp cleanupResp) string {
+ parts := []string{
+ fmt.Sprintf("gelöscht: %d/%d", resp.DeletedFiles, resp.ScannedFiles),
}
- return fmt.Sprintf(
- "gelöscht: %d/%d · freigegeben: %s",
- deleted,
- total,
- formatBytesSI(freedBytes),
- )
+ if resp.DeletedBrokenFiles > 0 {
+ parts = append(parts, fmt.Sprintf("defekt: %d", resp.DeletedBrokenFiles))
+ }
+
+ if resp.DeletedRecordOrphans > 0 {
+ parts = append(parts, fmt.Sprintf("Records: %d", resp.DeletedRecordOrphans))
+ }
+
+ if resp.DeletedTSOrphans > 0 {
+ parts = append(parts, fmt.Sprintf("TS ohne Audio: %d", resp.DeletedTSOrphans))
+ }
+
+ if resp.DeletedAudioOrphans > 0 {
+ parts = append(parts, fmt.Sprintf("Audio ohne TS: %d", resp.DeletedAudioOrphans))
+ }
+
+ if resp.DeletedMuxingTemps > 0 {
+ parts = append(parts, fmt.Sprintf("Muxing-Temp: %d", resp.DeletedMuxingTemps))
+ }
+
+ if resp.GeneratedOrphansRemoved > 0 {
+ parts = append(parts, fmt.Sprintf("Generated: %d", resp.GeneratedOrphansRemoved))
+ }
+
+ if resp.ErrorCount > 0 {
+ parts = append(parts, fmt.Sprintf("Fehler: %d", resp.ErrorCount))
+ }
+
+ parts = append(parts, fmt.Sprintf("freigegeben: %s", formatBytesSI(resp.DeletedBytes)))
+
+ return strings.Join(parts, " · ")
}
func cleanupVideoLooksBroken(ctx context.Context, path string) (bool, string) {
@@ -444,10 +468,14 @@ func cleanupDeleteRecordOrphanFile(ctx context.Context, jobID string, path strin
}
fi, err := os.Stat(path)
- if err != nil || fi == nil || fi.IsDir() || fi.Size() <= 0 {
+ if err != nil || fi == nil || fi.IsDir() {
return
}
+ size := fi.Size()
+ name := filepath.Base(path)
+ lowName := strings.ToLower(name)
+
if time.Since(fi.ModTime()) < cleanupRecordOrphanMinAge {
resp.SkippedFiles++
return
@@ -458,38 +486,49 @@ func cleanupDeleteRecordOrphanFile(ctx context.Context, jobID string, path strin
return
}
- if !cleanupFileStable(path) {
+ // 0-KB-Dateien sind stabil genug zum Löschen.
+ // Für >0 Bytes weiter die Stabilitätsprüfung behalten.
+ if size > 0 && !cleanupFileStable(path) {
resp.SkippedFiles++
return
}
updateCleanupJobState(jobID, func(st *CleanupTaskState) {
- st.CurrentFile = filepath.Base(path)
- st.Text = "Räume verwaiste Record-Dateien auf…"
+ st.CurrentFile = name
+ st.Text = "Räume Record-Reste auf…"
})
if err := removeWithRetry(path); err != nil && !os.IsNotExist(err) {
resp.ErrorCount++
- appLogln("⚠️ cleanup record orphan konnte nicht gelöscht werden:", filepath.Base(path), reason, err)
+ appLogln("⚠️ cleanup record orphan konnte nicht gelöscht werden:", name, reason, err)
return
}
resp.DeletedFiles++
resp.DeletedRecordOrphans++
- resp.DeletedBytes += fi.Size()
- resp.DeletedRecordOrphanBytes += fi.Size()
+ resp.DeletedBytes += size
+ resp.DeletedRecordOrphanBytes += size
+
+ if strings.HasSuffix(lowName, ".muxing.ts") {
+ resp.DeletedMuxingTemps++
+ } else if strings.HasPrefix(name, ".") && strings.HasSuffix(lowName, ".audio.m4s") {
+ resp.DeletedAudioOrphans++
+ } else if !strings.HasPrefix(name, ".") && strings.EqualFold(filepath.Ext(name), ".ts") {
+ resp.DeletedTSOrphans++
+ }
purgeDurationCacheForPath(path)
base := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
base = strings.TrimPrefix(base, ".")
base = strings.TrimSuffix(base, ".audio")
+ base = strings.TrimSuffix(base, ".muxing")
if strings.TrimSpace(base) != "" {
removeGeneratedForID(stripHotPrefix(base))
}
- appLogln("🧹 cleanup deleted record orphan:", filepath.Base(path), "reason:", reason)
+ appLogln("🧹 cleanup deleted record orphan:", name, "reason:", reason, "size=", size)
}
func cleanupRecordDirOrphanAVFiles(ctx context.Context, jobID string, recordAbs string, resp *cleanupResp) error {
@@ -522,6 +561,14 @@ func cleanupRecordDirOrphanAVFiles(ctx context.Context, jobID string, recordAbs
full := filepath.Join(recordAbs, name)
low := strings.ToLower(name)
+ // Fall 0:
+ // alte .muxing.ts sind temporäre Remux-/Muxing-Reste.
+ if strings.HasSuffix(low, ".muxing.ts") {
+ resp.ScannedFiles++
+ cleanupDeleteRecordOrphanFile(ctx, jobID, full, resp, "stale muxing temp")
+ continue
+ }
+
// Fall 1:
// foo.ts existiert, aber .foo.audio.m4s fehlt.
if !strings.HasPrefix(name, ".") && strings.EqualFold(filepath.Ext(name), ".ts") {
@@ -658,7 +705,7 @@ func cleanupSmallFiles(ctx context.Context, jobID string, doneAbs string, thresh
st.Done = 0
st.Total = resp.ScannedFiles
st.CurrentFile = ""
- st.Text = cleanupProgressText(resp.DeletedFiles, resp.DeletedBrokenFiles, resp.ScannedFiles, resp.DeletedBytes)
+ st.Text = cleanupProgressText(*resp)
st.Error = ""
st.FinishedAt = nil
})
@@ -716,7 +763,7 @@ func cleanupSmallFiles(ctx context.Context, jobID string, doneAbs string, thresh
st.Done = i + 1
st.Total = resp.ScannedFiles
st.CurrentFile = c.name
- st.Text = cleanupProgressText(resp.DeletedFiles, resp.DeletedBrokenFiles, resp.ScannedFiles, resp.DeletedBytes)
+ st.Text = cleanupProgressText(*resp)
st.Error = ""
st.FinishedAt = nil
})
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index f4f58b9..843a9c2 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -69,11 +69,23 @@ const DEFAULT_RECORDER_SETTINGS: RecorderSettingsState = {
useMyFreeCamsWatcher: false,
autoDeleteSmallDownloads: false,
autoDeleteSmallDownloadsBelowMB: 200,
+ autoDeleteSmallDownloadsKeepFavorites: true,
blurPreviews: false,
teaserPlayback: 'hover',
teaserAudio: false,
lowDiskPauseBelowGB: 5,
enableNotifications: true,
+
+ generateAssetsMeta: true,
+ generateAssetsThumb: true,
+ generateAssetsTeaser: true,
+ generateAssetsSprites: true,
+ generateAssetsAnalyze: true,
+
+ enrichPostworkEnabled: true,
+
+ trainingRecognitionEnabled: true,
+ trainingDetectorEpochs: 60,
}
type StoredModel = {
@@ -3812,14 +3824,30 @@ export default function App() {
return