// backend\settings.go package main import ( "encoding/json" "fmt" "net/http" "os" "path/filepath" "strings" "sync" "github.com/sqweek/dialog" ) type RecorderSettings struct { RecordDir string `json:"recordDir"` DoneDir string `json:"doneDir"` FFmpegPath string `json:"ffmpegPath"` AutoAddToDownloadList bool `json:"autoAddToDownloadList"` AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"` UseChaturbateAPI bool `json:"useChaturbateApi"` UseMyFreeCamsWatcher bool `json:"useMyFreeCamsWatcher"` // Wenn aktiv, werden fertige Downloads automatisch gelöscht, wenn sie kleiner als der Grenzwert sind. AutoDeleteSmallDownloads bool `json:"autoDeleteSmallDownloads"` AutoDeleteSmallDownloadsBelowMB int `json:"autoDeleteSmallDownloadsBelowMB"` BlurPreviews bool `json:"blurPreviews"` TeaserPlayback string `json:"teaserPlayback"` // still | hover | all TeaserAudio bool `json:"teaserAudio"` // ✅ Vorschau/Teaser mit Ton abspielen EnableNotifications bool `json:"enableNotifications"` // EncryptedCookies contains base64(nonce+ciphertext) of a JSON cookie map. EncryptedCookies string `json:"encryptedCookies"` } var ( settingsMu sync.Mutex settings = RecorderSettings{ RecordDir: "/records", DoneDir: "/records/done", FFmpegPath: "", AutoAddToDownloadList: false, AutoStartAddedDownloads: false, UseChaturbateAPI: false, UseMyFreeCamsWatcher: false, AutoDeleteSmallDownloads: false, AutoDeleteSmallDownloadsBelowMB: 50, BlurPreviews: false, TeaserPlayback: "hover", TeaserAudio: false, EnableNotifications: true, EncryptedCookies: "", } settingsFile = "recorder_settings.json" ) func settingsFilePath() string { // optionaler Override per ENV name := strings.TrimSpace(os.Getenv("RECORDER_SETTINGS_FILE")) if name == "" { name = settingsFile } // Standard: relativ zur EXE / App-Dir (oder fallback auf Working Dir bei go run) if p, err := resolvePathRelativeToApp(name); err == nil && strings.TrimSpace(p) != "" { return p } // Fallback: so zurückgeben wie es ist return name } func getSettings() RecorderSettings { settingsMu.Lock() defer settingsMu.Unlock() return settings } func loadSettings() { p := settingsFilePath() b, err := os.ReadFile(p) fmt.Println("🔧 settingsFile:", p) if err == nil { s := getSettings() // ✅ startet mit Defaults if json.Unmarshal(b, &s) == nil { if strings.TrimSpace(s.RecordDir) != "" { s.RecordDir = filepath.Clean(strings.TrimSpace(s.RecordDir)) } if strings.TrimSpace(s.DoneDir) != "" { s.DoneDir = filepath.Clean(strings.TrimSpace(s.DoneDir)) } if strings.TrimSpace(s.FFmpegPath) != "" { s.FFmpegPath = strings.TrimSpace(s.FFmpegPath) } s.TeaserPlayback = strings.ToLower(strings.TrimSpace(s.TeaserPlayback)) if s.TeaserPlayback == "" { s.TeaserPlayback = "hover" } if s.TeaserPlayback != "still" && s.TeaserPlayback != "hover" && s.TeaserPlayback != "all" { s.TeaserPlayback = "hover" } // Auto-Delete: clamp if s.AutoDeleteSmallDownloadsBelowMB < 0 { s.AutoDeleteSmallDownloadsBelowMB = 0 } if s.AutoDeleteSmallDownloadsBelowMB > 100_000 { s.AutoDeleteSmallDownloadsBelowMB = 100_000 } settingsMu.Lock() settings = s settingsMu.Unlock() } } // Ordner sicherstellen s := getSettings() recordAbs, _ := resolvePathRelativeToApp(s.RecordDir) doneAbs, _ := resolvePathRelativeToApp(s.DoneDir) if strings.TrimSpace(recordAbs) != "" { _ = os.MkdirAll(recordAbs, 0o755) } if strings.TrimSpace(doneAbs) != "" { _ = os.MkdirAll(doneAbs, 0o755) } // ffmpeg-Pfad anhand Settings/Env/PATH bestimmen ffmpegPath = detectFFmpegPath() fmt.Println("🔍 ffmpegPath:", ffmpegPath) ffprobePath = detectFFprobePath() fmt.Println("🔍 ffprobePath:", ffprobePath) } func saveSettingsToDisk() { s := getSettings() b, err := json.MarshalIndent(s, "", " ") if err != nil { fmt.Println("⚠️ settings marshal:", err) return } b = append(b, '\n') p := settingsFilePath() if err := atomicWriteFile(p, b); err != nil { fmt.Println("⚠️ settings write:", err) return } // optional // fmt.Println("✅ settings saved:", p) } func recordSettingsHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(getSettings()) return case http.MethodPost: var in RecorderSettings if err := json.NewDecoder(r.Body).Decode(&in); err != nil { http.Error(w, "invalid json: "+err.Error(), http.StatusBadRequest) return } // --- normalize (WICHTIG: erst trim, dann leer-check, dann clean) --- recRaw := strings.TrimSpace(in.RecordDir) doneRaw := strings.TrimSpace(in.DoneDir) if recRaw == "" || doneRaw == "" { http.Error(w, "recordDir und doneDir dürfen nicht leer sein", http.StatusBadRequest) return } in.RecordDir = filepath.Clean(recRaw) in.DoneDir = filepath.Clean(doneRaw) // Optional aber sehr empfehlenswert: "." verbieten if in.RecordDir == "." || in.DoneDir == "." { http.Error(w, "recordDir/doneDir dürfen nicht '.' sein", http.StatusBadRequest) return } in.FFmpegPath = strings.TrimSpace(in.FFmpegPath) in.TeaserPlayback = strings.ToLower(strings.TrimSpace(in.TeaserPlayback)) if in.TeaserPlayback == "" { in.TeaserPlayback = "hover" } if in.TeaserPlayback != "still" && in.TeaserPlayback != "hover" && in.TeaserPlayback != "all" { in.TeaserPlayback = "hover" } // Auto-Delete: clamp if in.AutoDeleteSmallDownloadsBelowMB < 0 { in.AutoDeleteSmallDownloadsBelowMB = 0 } if in.AutoDeleteSmallDownloadsBelowMB > 100_000 { in.AutoDeleteSmallDownloadsBelowMB = 100_000 } // --- ensure folders (Fehler zurückgeben, falls z.B. keine Rechte) --- recAbs, err := resolvePathRelativeToApp(in.RecordDir) if err != nil { http.Error(w, "ungültiger recordDir: "+err.Error(), http.StatusBadRequest) return } doneAbs, err := resolvePathRelativeToApp(in.DoneDir) if err != nil { http.Error(w, "ungültiger doneDir: "+err.Error(), http.StatusBadRequest) return } if err := os.MkdirAll(recAbs, 0o755); err != nil { http.Error(w, "konnte recordDir nicht erstellen: "+err.Error(), http.StatusBadRequest) return } if err := os.MkdirAll(doneAbs, 0o755); err != nil { http.Error(w, "konnte doneDir nicht erstellen: "+err.Error(), http.StatusBadRequest) return } // ✅ Settings im RAM aktualisieren settingsMu.Lock() settings = in settingsMu.Unlock() // ✅ Settings auf Disk persistieren saveSettingsToDisk() // ✅ ffmpeg/ffprobe nach Änderungen neu bestimmen // Tipp: wenn der User FFmpegPath explizit setzt, nutze den direkt. if strings.TrimSpace(in.FFmpegPath) != "" { ffmpegPath = in.FFmpegPath } else { ffmpegPath = detectFFmpegPath() } fmt.Println("🔍 ffmpegPath:", ffmpegPath) ffprobePath = detectFFprobePath() fmt.Println("🔍 ffprobePath:", ffprobePath) w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(getSettings()) return default: http.Error(w, "Nur GET/POST erlaubt", http.StatusMethodNotAllowed) return } } func settingsBrowse(w http.ResponseWriter, r *http.Request) { target := r.URL.Query().Get("target") if target != "record" && target != "done" && target != "ffmpeg" { http.Error(w, "target muss record, done oder ffmpeg sein", http.StatusBadRequest) return } var ( p string err error ) if target == "ffmpeg" { // Dateiauswahl für ffmpeg.exe p, err = dialog.File(). Title("ffmpeg.exe auswählen"). Load() } else { // Ordnerauswahl für record/done p, err = dialog.Directory(). Title("Ordner auswählen"). Browse() } if err != nil { // User cancelled → 204 No Content ist praktisch fürs Frontend if strings.Contains(strings.ToLower(err.Error()), "cancel") { w.WriteHeader(http.StatusNoContent) return } http.Error(w, "auswahl fehlgeschlagen: "+err.Error(), http.StatusInternalServerError) return } // optional: wenn innerhalb exe-dir, als RELATIV zurückgeben p = maybeMakeRelativeToExe(p) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"path": p}) } func maybeMakeRelativeToExe(abs string) string { exe, err := os.Executable() if err != nil { return abs } base := filepath.Dir(exe) rel, err := filepath.Rel(base, abs) if err != nil { return abs } // wenn rel mit ".." beginnt -> nicht innerhalb base -> absoluten Pfad behalten if rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { return abs } return filepath.ToSlash(rel) // frontend-freundlich }