nsfwapp/backend/settings.go
2026-06-23 15:22:08 +02:00

705 lines
22 KiB
Go

// backend\settings.go
package main
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/sqweek/dialog"
)
type RecorderSettings struct {
DatabaseType string `json:"databaseType"` // sqlite | postgres
SQLitePath string `json:"sqlitePath"`
DatabaseURL string `json:"databaseUrl"`
EncryptedDBPassword string `json:"encryptedDbPassword,omitempty"` // base64(nonce+ciphertext)
RecordDir string `json:"recordDir"`
DoneDir string `json:"doneDir"`
FFmpegPath string `json:"ffmpegPath"`
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"`
EnableConcurrentDownloadsLimit bool `json:"enableConcurrentDownloadsLimit"`
MaxConcurrentDownloads int `json:"maxConcurrentDownloads"`
UseProviderAPIs bool `json:"useProviderApis"`
UseChaturbateAPI bool `json:"useChaturbateApi"`
UseMyFreeCamsAPI bool `json:"useMyFreeCamsApi"`
// Wenn aktiv, werden fertige Downloads automatisch gelöscht, wenn sie kleiner als der Grenzwert sind.
AutoDeleteSmallDownloads bool `json:"autoDeleteSmallDownloads"`
AutoDeleteSmallDownloadsBelowMB int `json:"autoDeleteSmallDownloadsBelowMB"`
AutoDeleteSmallDownloadsKeepFavorites bool `json:"autoDeleteSmallDownloadsKeepFavorites"`
// Wenn aktiv, werden Downloads nach erfolgreicher Analyse gelöscht,
// wenn ihr Rating <= AutoDeleteLowRatedDownloadsMaxStars ist.
AutoDeleteLowRatedDownloads bool `json:"autoDeleteLowRatedDownloads"`
AutoDeleteLowRatedDownloadsMaxStars int `json:"autoDeleteLowRatedDownloadsMaxStars"`
LowDiskPauseBelowGB int `json:"lowDiskPauseBelowGB"`
BlurPreviews bool `json:"blurPreviews"`
TeaserPlayback string `json:"teaserPlayback"` // still | hover | all
TeaserAudio bool `json:"teaserAudio"` // ✅ Vorschau/Teaser mit Ton abspielen
EnableNotifications bool `json:"enableNotifications"`
GenerateAssetsMeta bool `json:"generateAssetsMeta"`
GenerateAssetsThumb bool `json:"generateAssetsThumb"`
GenerateAssetsTeaser bool `json:"generateAssetsTeaser"`
GenerateAssetsSprites bool `json:"generateAssetsSprites"`
GenerateAssetsAnalyze bool `json:"generateAssetsAnalyze"`
EnrichPostworkEnabled bool `json:"enrichPostworkEnabled"`
TrainingRecognitionEnabled bool `json:"trainingRecognitionEnabled"`
TrainingDetectorEpochs int `json:"trainingDetectorEpochs"`
// EncryptedCookies contains base64(nonce+ciphertext) of a JSON cookie map.
EncryptedCookies string `json:"encryptedCookies"`
}
var (
settingsMu sync.Mutex
settings = RecorderSettings{
DatabaseType: databaseTypeSQLite,
SQLitePath: defaultSQLitePath,
DatabaseURL: "",
EncryptedDBPassword: "",
RecordDir: defaultRecordDir,
DoneDir: defaultDoneDir,
FFmpegPath: "",
AutoAddToDownloadList: false,
AutoStartAddedDownloads: true,
EnableConcurrentDownloadsLimit: false,
MaxConcurrentDownloads: 20,
UseProviderAPIs: false,
UseChaturbateAPI: false,
UseMyFreeCamsAPI: false,
AutoDeleteSmallDownloads: false,
AutoDeleteSmallDownloadsBelowMB: 50,
AutoDeleteSmallDownloadsKeepFavorites: true,
AutoDeleteLowRatedDownloads: false,
AutoDeleteLowRatedDownloadsMaxStars: 3,
LowDiskPauseBelowGB: 5,
BlurPreviews: false,
TeaserPlayback: "hover",
TeaserAudio: false,
EnableNotifications: true,
GenerateAssetsMeta: true,
GenerateAssetsThumb: true,
GenerateAssetsTeaser: true,
GenerateAssetsSprites: false,
GenerateAssetsAnalyze: false,
EnrichPostworkEnabled: true,
TrainingRecognitionEnabled: true,
TrainingDetectorEpochs: 60,
EncryptedCookies: "",
}
settingsFile = "recorder_settings.json"
)
const (
defaultRecordDir = "record"
defaultDoneDir = "record/done"
legacyDefaultRecordDir = "/records"
legacyDefaultDoneDir = "/records/done"
)
func normalizeSettingsDir(raw string, fallback string, legacyDefaults ...string) string {
path := strings.TrimSpace(raw)
if path == "" {
return fallback
}
path = filepath.Clean(filepath.FromSlash(path))
for _, legacy := range legacyDefaults {
if path == filepath.Clean(filepath.FromSlash(strings.TrimSpace(legacy))) {
return fallback
}
}
return path
}
func settingsFilePath() string {
// optionaler Override per ENV
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)
appLogln("🔧 settingsFile:", p)
if err == nil {
s := getSettings() // ✅ startet mit Defaults
if json.Unmarshal(b, &s) == nil {
s.RecordDir = normalizeSettingsDir(s.RecordDir, defaultRecordDir, legacyDefaultRecordDir)
s.DoneDir = normalizeSettingsDir(s.DoneDir, defaultDoneDir, legacyDefaultDoneDir)
s.DatabaseType = normalizeDatabaseType(s.DatabaseType)
s.SQLitePath = normalizeSQLitePath(s.SQLitePath)
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
}
if s.AutoDeleteLowRatedDownloadsMaxStars < 1 {
s.AutoDeleteLowRatedDownloadsMaxStars = 3
}
if s.AutoDeleteLowRatedDownloadsMaxStars > 5 {
s.AutoDeleteLowRatedDownloadsMaxStars = 5
}
if s.LowDiskPauseBelowGB < 1 {
s.LowDiskPauseBelowGB = 1
}
if s.LowDiskPauseBelowGB > 10_000 {
s.LowDiskPauseBelowGB = 10_000
}
if s.TrainingDetectorEpochs < 1 {
s.TrainingDetectorEpochs = 60
}
if s.TrainingDetectorEpochs > 300 {
s.TrainingDetectorEpochs = 300
}
settingsMu.Lock()
settings = s
settingsMu.Unlock()
}
s.DatabaseType = normalizeDatabaseType(s.DatabaseType)
s.SQLitePath = normalizeSQLitePath(s.SQLitePath)
s.DatabaseURL = strings.TrimSpace(s.DatabaseURL)
// Optional: falls in der JSON mal ein URL MIT Passwort steht (Altbestand)
// -> Passwort extrahieren und verschlüsselt ablegen (nur wenn noch keins gesetzt ist)
if s.DatabaseURL != "" && strings.TrimSpace(s.EncryptedDBPassword) == "" {
sanitizedURL, pwFromURL := stripPasswordFromPostgresURL(s.DatabaseURL)
if sanitizedURL != "" {
s.DatabaseURL = sanitizedURL
}
if strings.TrimSpace(pwFromURL) != "" {
if enc, err := encryptSettingString(strings.TrimSpace(pwFromURL)); err == nil {
s.EncryptedDBPassword = enc
}
}
}
// ✅ WICHTIG: Migrationsergebnis zurück in den globalen settings-State schreiben
settingsMu.Lock()
settings = s
settingsMu.Unlock()
// optional aber sinnvoll: Migration auch persistieren
saveSettingsToDisk()
}
// 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()
appLogln("🔍 ffmpegPath:", ffmpegPath)
ffprobePath = detectFFprobePath()
appLogln("🔍 ffprobePath:", ffprobePath)
}
func saveSettingsToDisk() {
s := getSettings()
b, err := json.MarshalIndent(s, "", " ")
if err != nil {
appLogln("⚠️ settings marshal:", err)
return
}
b = append(b, '\n')
p := settingsFilePath()
if err := atomicWriteFile(p, b); err != nil {
appLogln("⚠️ settings write:", err)
return
}
// optional
// appLogln("✅ settings saved:", p)
}
type RecorderSettingsPublic struct {
RecordDir string `json:"recordDir"`
DoneDir string `json:"doneDir"`
FFmpegPath string `json:"ffmpegPath"`
DatabaseType string `json:"databaseType"`
SQLitePath string `json:"sqlitePath"`
DatabaseURL string `json:"databaseUrl"`
HasDBPassword bool `json:"hasDbPassword"`
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"`
EnableConcurrentDownloadsLimit bool `json:"enableConcurrentDownloadsLimit"`
MaxConcurrentDownloads int `json:"maxConcurrentDownloads"`
UseProviderAPIs bool `json:"useProviderApis"`
UseChaturbateAPI bool `json:"useChaturbateApi"`
UseMyFreeCamsAPI bool `json:"useMyFreeCamsApi"`
AutoDeleteSmallDownloads bool `json:"autoDeleteSmallDownloads"`
AutoDeleteSmallDownloadsBelowMB int `json:"autoDeleteSmallDownloadsBelowMB"`
AutoDeleteSmallDownloadsKeepFavorites bool `json:"autoDeleteSmallDownloadsKeepFavorites"`
AutoDeleteLowRatedDownloads bool `json:"autoDeleteLowRatedDownloads"`
AutoDeleteLowRatedDownloadsMaxStars int `json:"autoDeleteLowRatedDownloadsMaxStars"`
LowDiskPauseBelowGB int `json:"lowDiskPauseBelowGB"`
BlurPreviews bool `json:"blurPreviews"`
TeaserPlayback string `json:"teaserPlayback"`
TeaserAudio bool `json:"teaserAudio"`
EnableNotifications bool `json:"enableNotifications"`
GenerateAssetsMeta bool `json:"generateAssetsMeta"`
GenerateAssetsThumb bool `json:"generateAssetsThumb"`
GenerateAssetsTeaser bool `json:"generateAssetsTeaser"`
GenerateAssetsSprites bool `json:"generateAssetsSprites"`
GenerateAssetsAnalyze bool `json:"generateAssetsAnalyze"`
EnrichPostworkEnabled bool `json:"enrichPostworkEnabled"`
TrainingRecognitionEnabled bool `json:"trainingRecognitionEnabled"`
TrainingDetectorEpochs int `json:"trainingDetectorEpochs"`
}
func toPublicSettings(s RecorderSettings) RecorderSettingsPublic {
return RecorderSettingsPublic{
RecordDir: s.RecordDir,
DoneDir: s.DoneDir,
FFmpegPath: s.FFmpegPath,
DatabaseType: normalizeDatabaseType(s.DatabaseType),
SQLitePath: normalizeSQLitePath(s.SQLitePath),
DatabaseURL: strings.TrimSpace(s.DatabaseURL),
HasDBPassword: strings.TrimSpace(s.EncryptedDBPassword) != "",
AutoAddToDownloadList: s.AutoAddToDownloadList,
AutoStartAddedDownloads: s.AutoStartAddedDownloads,
EnableConcurrentDownloadsLimit: s.EnableConcurrentDownloadsLimit,
MaxConcurrentDownloads: s.MaxConcurrentDownloads,
UseProviderAPIs: s.UseProviderAPIs,
UseChaturbateAPI: s.UseChaturbateAPI,
UseMyFreeCamsAPI: s.UseMyFreeCamsAPI,
AutoDeleteSmallDownloads: s.AutoDeleteSmallDownloads,
AutoDeleteSmallDownloadsBelowMB: s.AutoDeleteSmallDownloadsBelowMB,
AutoDeleteSmallDownloadsKeepFavorites: s.AutoDeleteSmallDownloadsKeepFavorites,
AutoDeleteLowRatedDownloads: s.AutoDeleteLowRatedDownloads,
AutoDeleteLowRatedDownloadsMaxStars: s.AutoDeleteLowRatedDownloadsMaxStars,
LowDiskPauseBelowGB: s.LowDiskPauseBelowGB,
BlurPreviews: s.BlurPreviews,
TeaserPlayback: s.TeaserPlayback,
TeaserAudio: s.TeaserAudio,
EnableNotifications: s.EnableNotifications,
GenerateAssetsMeta: s.GenerateAssetsMeta,
GenerateAssetsThumb: s.GenerateAssetsThumb,
GenerateAssetsTeaser: s.GenerateAssetsTeaser,
GenerateAssetsSprites: s.GenerateAssetsSprites,
GenerateAssetsAnalyze: s.GenerateAssetsAnalyze,
EnrichPostworkEnabled: s.EnrichPostworkEnabled,
TrainingRecognitionEnabled: s.TrainingRecognitionEnabled,
TrainingDetectorEpochs: s.TrainingDetectorEpochs,
}
}
type RecorderSettingsIn struct {
RecorderSettings
DBPassword string `json:"dbPassword,omitempty"` // nur vom Frontend, NIE auf Disk speichern
}
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(toPublicSettings(getSettings()))
return
case http.MethodPost:
var in RecorderSettingsIn
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
}
if in.AutoDeleteLowRatedDownloadsMaxStars < 1 {
in.AutoDeleteLowRatedDownloadsMaxStars = 3
}
if in.AutoDeleteLowRatedDownloadsMaxStars > 5 {
in.AutoDeleteLowRatedDownloadsMaxStars = 5
}
if in.LowDiskPauseBelowGB < 1 {
in.LowDiskPauseBelowGB = 1
}
if in.LowDiskPauseBelowGB > 10_000 {
in.LowDiskPauseBelowGB = 10_000
}
if in.MaxConcurrentDownloads < 1 {
in.MaxConcurrentDownloads = 1
}
if in.TrainingDetectorEpochs < 1 {
in.TrainingDetectorEpochs = 60
}
if in.TrainingDetectorEpochs > 300 {
in.TrainingDetectorEpochs = 300
}
// --- 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
}
// --- DB URL + Passwort behandeln ---
// 1) Trim
in.DatabaseType = normalizeDatabaseType(in.DatabaseType)
in.SQLitePath = normalizeSQLitePath(in.SQLitePath)
in.DatabaseURL = strings.TrimSpace(in.DatabaseURL)
// 2) Migration: wenn in.DatabaseURL ein Passwort enthält, extrahieren
// und URL ohne Passwort zurückschreiben.
sanitizedURL, pwFromURL := stripPasswordFromPostgresURL(in.DatabaseURL)
pwFromURL = strings.TrimSpace(pwFromURL)
if pwFromURL == "****" {
pwFromURL = ""
}
if sanitizedURL != "" {
in.DatabaseURL = sanitizedURL
}
// 3) Wenn Frontend ein Passwort sendet, hat das Priorität.
current := getSettings()
plainPW := strings.TrimSpace(in.DBPassword)
if plainPW == "" {
plainPW = pwFromURL
}
if plainPW != "" {
enc, err := encryptSettingString(plainPW)
if err != nil {
http.Error(w, "konnte DB-Passwort nicht verschlüsseln: "+err.Error(), http.StatusInternalServerError)
return
}
in.EncryptedDBPassword = enc
} else {
in.EncryptedDBPassword = current.EncryptedDBPassword
}
// Nicht komplette Settings stumpf überschreiben,
// sondern bestehende Settings nehmen und gezielt aktualisieren.
next := current
next.RecordDir = in.RecordDir
next.DoneDir = in.DoneDir
next.FFmpegPath = in.FFmpegPath
next.DatabaseType = in.DatabaseType
next.SQLitePath = in.SQLitePath
next.DatabaseURL = in.DatabaseURL
next.EncryptedDBPassword = in.EncryptedDBPassword
next.AutoAddToDownloadList = in.AutoAddToDownloadList
next.AutoStartAddedDownloads = in.AutoStartAddedDownloads
next.EnableConcurrentDownloadsLimit = in.EnableConcurrentDownloadsLimit
next.MaxConcurrentDownloads = in.MaxConcurrentDownloads
next.UseProviderAPIs = in.UseProviderAPIs
next.UseChaturbateAPI = in.UseChaturbateAPI
next.UseMyFreeCamsAPI = in.UseMyFreeCamsAPI
next.AutoDeleteSmallDownloads = in.AutoDeleteSmallDownloads
next.AutoDeleteSmallDownloadsBelowMB = in.AutoDeleteSmallDownloadsBelowMB
next.AutoDeleteSmallDownloadsKeepFavorites = in.AutoDeleteSmallDownloadsKeepFavorites
next.AutoDeleteLowRatedDownloads = in.AutoDeleteLowRatedDownloads
next.AutoDeleteLowRatedDownloadsMaxStars = in.AutoDeleteLowRatedDownloadsMaxStars
next.LowDiskPauseBelowGB = in.LowDiskPauseBelowGB
next.BlurPreviews = in.BlurPreviews
next.TeaserPlayback = in.TeaserPlayback
next.TeaserAudio = in.TeaserAudio
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
dbChanged :=
normalizeDatabaseType(next.DatabaseType) != normalizeDatabaseType(current.DatabaseType) ||
normalizeSQLitePath(next.SQLitePath) != normalizeSQLitePath(current.SQLitePath) ||
strings.TrimSpace(next.DatabaseURL) != strings.TrimSpace(current.DatabaseURL) ||
strings.TrimSpace(next.EncryptedDBPassword) != strings.TrimSpace(current.EncryptedDBPassword)
var newStore *ModelStore
// DB erst prüfen, BEVOR gespeichert wird
if dbChanged {
sqliteMoved := false
if sqlitePathChanged(current, next) {
if err := moveSQLiteDatabaseForSettings(current, next); err != nil {
http.Error(w, "SQLite-Datei konnte nicht verschoben werden: "+err.Error(), http.StatusBadRequest)
return
}
sqliteMoved = true
}
storeConfig, err := buildModelStoreConfigFromRecorderSettings(next)
if err != nil {
if sqliteMoved {
_ = moveSQLiteDatabaseForSettings(next, current)
restoreModelStoreForSettings(current)
}
http.Error(w, "ungültige Datenbank-Konfiguration: "+err.Error(), http.StatusBadRequest)
return
}
newStore = NewModelStore(storeConfig)
if err := newStore.Load(); err != nil {
_ = newStore.Close()
if sqliteMoved {
_ = moveSQLiteDatabaseForSettings(next, current)
restoreModelStoreForSettings(current)
}
http.Error(w, "Datenbank-Verbindung fehlgeschlagen: "+err.Error(), http.StatusBadRequest)
return
}
}
// Erst jetzt wirklich übernehmen
settingsMu.Lock()
settings = next
settingsMu.Unlock()
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)
}
// ✅ 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()
}
//appLogln("🔍 ffmpegPath:", ffmpegPath)
ffprobePath = detectFFprobePath()
//appLogln("🔍 ffprobePath:", ffprobePath)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(toPublicSettings(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" && target != "sqlite" {
http.Error(w, "target muss record, done, ffmpeg oder sqlite 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 if target == "sqlite" {
p, err = dialog.Directory().
Title("SQLite-Speicherort auswaehlen").
Browse()
} 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
}