600 lines
18 KiB
Go
600 lines
18 KiB
Go
// backend\settings.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/sqweek/dialog"
|
|
)
|
|
|
|
type RecorderSettings struct {
|
|
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"`
|
|
EnableConcurrentDownloadsLimit bool `json:"enableConcurrentDownloadsLimit"`
|
|
MaxConcurrentDownloads int `json:"maxConcurrentDownloads"`
|
|
|
|
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"`
|
|
AutoDeleteSmallDownloadsKeepFavorites bool `json:"autoDeleteSmallDownloadsKeepFavorites"`
|
|
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{
|
|
DatabaseURL: "",
|
|
EncryptedDBPassword: "",
|
|
|
|
RecordDir: "/records",
|
|
DoneDir: "/records/done",
|
|
FFmpegPath: "",
|
|
|
|
AutoAddToDownloadList: false,
|
|
EnableConcurrentDownloadsLimit: false,
|
|
MaxConcurrentDownloads: 20,
|
|
|
|
UseChaturbateAPI: false,
|
|
UseMyFreeCamsWatcher: false,
|
|
AutoDeleteSmallDownloads: false,
|
|
AutoDeleteSmallDownloadsBelowMB: 50,
|
|
AutoDeleteSmallDownloadsKeepFavorites: true,
|
|
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"
|
|
)
|
|
|
|
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 {
|
|
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
|
|
}
|
|
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.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"`
|
|
|
|
DatabaseURL string `json:"databaseUrl"`
|
|
HasDBPassword bool `json:"hasDbPassword"`
|
|
|
|
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
|
|
EnableConcurrentDownloadsLimit bool `json:"enableConcurrentDownloadsLimit"`
|
|
MaxConcurrentDownloads int `json:"maxConcurrentDownloads"`
|
|
|
|
UseChaturbateAPI bool `json:"useChaturbateApi"`
|
|
UseMyFreeCamsWatcher bool `json:"useMyFreeCamsWatcher"`
|
|
|
|
AutoDeleteSmallDownloads bool `json:"autoDeleteSmallDownloads"`
|
|
AutoDeleteSmallDownloadsBelowMB int `json:"autoDeleteSmallDownloadsBelowMB"`
|
|
AutoDeleteSmallDownloadsKeepFavorites bool `json:"autoDeleteSmallDownloadsKeepFavorites"`
|
|
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,
|
|
|
|
DatabaseURL: strings.TrimSpace(s.DatabaseURL),
|
|
HasDBPassword: strings.TrimSpace(s.EncryptedDBPassword) != "",
|
|
|
|
AutoAddToDownloadList: s.AutoAddToDownloadList,
|
|
EnableConcurrentDownloadsLimit: s.EnableConcurrentDownloadsLimit,
|
|
MaxConcurrentDownloads: s.MaxConcurrentDownloads,
|
|
|
|
UseChaturbateAPI: s.UseChaturbateAPI,
|
|
UseMyFreeCamsWatcher: s.UseMyFreeCamsWatcher,
|
|
|
|
AutoDeleteSmallDownloads: s.AutoDeleteSmallDownloads,
|
|
AutoDeleteSmallDownloadsBelowMB: s.AutoDeleteSmallDownloadsBelowMB,
|
|
AutoDeleteSmallDownloadsKeepFavorites: s.AutoDeleteSmallDownloadsKeepFavorites,
|
|
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.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.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.DatabaseURL = in.DatabaseURL
|
|
next.EncryptedDBPassword = in.EncryptedDBPassword
|
|
|
|
next.AutoAddToDownloadList = in.AutoAddToDownloadList
|
|
next.EnableConcurrentDownloadsLimit = in.EnableConcurrentDownloadsLimit
|
|
next.MaxConcurrentDownloads = in.MaxConcurrentDownloads
|
|
|
|
next.UseChaturbateAPI = in.UseChaturbateAPI
|
|
next.UseMyFreeCamsWatcher = in.UseMyFreeCamsWatcher
|
|
|
|
next.AutoDeleteSmallDownloads = in.AutoDeleteSmallDownloads
|
|
next.AutoDeleteSmallDownloadsBelowMB = in.AutoDeleteSmallDownloadsBelowMB
|
|
next.AutoDeleteSmallDownloadsKeepFavorites = in.AutoDeleteSmallDownloadsKeepFavorites
|
|
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 :=
|
|
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 {
|
|
dsn, err := buildPostgresDSNFromRecorderSettings(next)
|
|
if err != nil {
|
|
http.Error(w, "ungültige Datenbank-Konfiguration: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
newStore = NewModelStore(dsn)
|
|
if err := newStore.Load(); err != nil {
|
|
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" {
|
|
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
|
|
}
|