Compare commits
No commits in common. "548919cde216877221d21420b647786bf8060af0" and "2385778c80b7f46eb564169baefdb2f653991155" have entirely different histories.
548919cde2
...
2385778c80
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,4 +2,3 @@
|
|||||||
backend/generated
|
backend/generated
|
||||||
backend/nsfwapp.exe
|
backend/nsfwapp.exe
|
||||||
backend/web/dist
|
backend/web/dist
|
||||||
backend/recorder_settings.json
|
|
||||||
|
|||||||
@ -96,28 +96,6 @@ type bulkFilesRequest struct {
|
|||||||
Files []string `json:"files"`
|
Files []string `json:"files"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type concurrentDownloadsStatusResp struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Max int `json:"max"`
|
|
||||||
Active int `json:"active"`
|
|
||||||
Reached bool `json:"reached"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func recordConcurrentDownloadsStatus(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !mustMethod(w, r, http.MethodGet) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
enabled, max, active, reached := concurrentDownloadsLimitState()
|
|
||||||
|
|
||||||
respondJSON(w, concurrentDownloadsStatusResp{
|
|
||||||
Enabled: enabled,
|
|
||||||
Max: max,
|
|
||||||
Active: active,
|
|
||||||
Reached: reached,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeUndoDeleteToken(t undoDeleteToken) (string, error) {
|
func encodeUndoDeleteToken(t undoDeleteToken) (string, error) {
|
||||||
b, err := json.Marshal(t)
|
b, err := json.Marshal(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -254,48 +254,6 @@ func recordPreviewSprite(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// ---------------- Start + run job ----------------
|
// ---------------- Start + run job ----------------
|
||||||
|
|
||||||
func isActiveRecordingJob(job *RecordJob) bool {
|
|
||||||
if job == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if job.EndedAt != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if job.Status != JobRunning {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
phase := strings.ToLower(strings.TrimSpace(job.Phase))
|
|
||||||
return phase == "" || phase == "recording"
|
|
||||||
}
|
|
||||||
|
|
||||||
func activeRecordingJobsCount() int {
|
|
||||||
jobsMu.Lock()
|
|
||||||
defer jobsMu.Unlock()
|
|
||||||
|
|
||||||
n := 0
|
|
||||||
for _, j := range jobs {
|
|
||||||
if isActiveRecordingJob(j) {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func concurrentDownloadsLimitState() (enabled bool, max int, active int, reached bool) {
|
|
||||||
s := getSettings()
|
|
||||||
|
|
||||||
enabled = s.EnableConcurrentDownloadsLimit
|
|
||||||
max = s.MaxConcurrentDownloads
|
|
||||||
if max < 1 {
|
|
||||||
max = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
active = activeRecordingJobsCount()
|
|
||||||
reached = enabled && active >= max
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func startRecordingInternal(req RecordRequest) (*RecordJob, error) {
|
func startRecordingInternal(req RecordRequest) (*RecordJob, error) {
|
||||||
url := strings.TrimSpace(req.URL)
|
url := strings.TrimSpace(req.URL)
|
||||||
if url == "" {
|
if url == "" {
|
||||||
@ -303,39 +261,19 @@ func startRecordingInternal(req RecordRequest) (*RecordJob, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jobsMu.Lock()
|
jobsMu.Lock()
|
||||||
defer jobsMu.Unlock()
|
|
||||||
|
|
||||||
// Duplicate-Check
|
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
if j != nil && j.Status == JobRunning && j.EndedAt == nil && strings.TrimSpace(j.SourceURL) == url {
|
if j != nil && j.Status == JobRunning && j.EndedAt == nil && strings.TrimSpace(j.SourceURL) == url {
|
||||||
if j.Hidden && !req.Hidden {
|
if j.Hidden && !req.Hidden {
|
||||||
j.Hidden = false
|
j.Hidden = false
|
||||||
|
jobsMu.Unlock()
|
||||||
return j, nil
|
return j, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobsMu.Unlock()
|
||||||
return j, nil
|
return j, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit-Check ATOMAR unter jobsMu
|
|
||||||
s := getSettings()
|
|
||||||
if s.EnableConcurrentDownloadsLimit {
|
|
||||||
max := s.MaxConcurrentDownloads
|
|
||||||
if max < 1 {
|
|
||||||
max = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
active := 0
|
|
||||||
for _, j := range jobs {
|
|
||||||
if isActiveRecordingJob(j) {
|
|
||||||
active++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if active >= max {
|
|
||||||
return nil, fmt.Errorf("Download-Limit erreicht (%d/%d aktiv)", active, max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startedAt := time.Now()
|
startedAt := time.Now()
|
||||||
provider := detectProvider(url)
|
provider := detectProvider(url)
|
||||||
|
|
||||||
@ -352,6 +290,7 @@ func startRecordingInternal(req RecordRequest) (*RecordJob, error) {
|
|||||||
|
|
||||||
filename := fmt.Sprintf("%s_%s.ts", username, startedAt.Format("01_02_2006__15-04-05"))
|
filename := fmt.Sprintf("%s_%s.ts", username, startedAt.Format("01_02_2006__15-04-05"))
|
||||||
|
|
||||||
|
s := getSettings()
|
||||||
recordDirAbs, _ := resolvePathRelativeToApp(s.RecordDir)
|
recordDirAbs, _ := resolvePathRelativeToApp(s.RecordDir)
|
||||||
recordDir := strings.TrimSpace(recordDirAbs)
|
recordDir := strings.TrimSpace(recordDirAbs)
|
||||||
if recordDir == "" {
|
if recordDir == "" {
|
||||||
@ -374,6 +313,7 @@ func startRecordingInternal(req RecordRequest) (*RecordJob, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jobs[jobID] = job
|
jobs[jobID] = job
|
||||||
|
jobsMu.Unlock()
|
||||||
|
|
||||||
go runJob(ctx, job, req)
|
go runJob(ctx, job, req)
|
||||||
return job, nil
|
return job, nil
|
||||||
@ -668,47 +608,16 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Remux
|
|
||||||
// 1) Remux
|
// 1) Remux
|
||||||
if strings.EqualFold(filepath.Ext(out), ".ts") {
|
if strings.EqualFold(filepath.Ext(out), ".ts") {
|
||||||
setPhase("remuxing", 10)
|
setPhase("remuxing", 10)
|
||||||
|
if newOut, err2 := maybeRemuxTSForJob(job, out); err2 == nil && strings.TrimSpace(newOut) != "" {
|
||||||
newOut, err2 := maybeRemuxTSForJob(job, out)
|
|
||||||
if err2 != nil || strings.TrimSpace(newOut) == "" {
|
|
||||||
finalFile := filepath.Base(out)
|
|
||||||
finalAssetID := assetIDFromVideoPath(out)
|
|
||||||
|
|
||||||
jobsMu.Lock()
|
|
||||||
job.Status = JobFailed
|
|
||||||
job.Error = "TS->MP4 Remux fehlgeschlagen"
|
|
||||||
if err2 != nil {
|
|
||||||
job.Error = "TS->MP4 Remux fehlgeschlagen: " + err2.Error()
|
|
||||||
}
|
|
||||||
job.Phase = ""
|
|
||||||
job.Progress = 100
|
|
||||||
job.PostWorkKey = ""
|
|
||||||
job.PostWork = nil
|
|
||||||
jobsMu.Unlock()
|
|
||||||
|
|
||||||
publishJobRemove(job)
|
|
||||||
|
|
||||||
publishFinishedPostworkStatusForJob(job, finishedPostworkEvent{
|
|
||||||
File: finalFile,
|
|
||||||
AssetID: finalAssetID,
|
|
||||||
Queue: "postwork",
|
|
||||||
State: "error",
|
|
||||||
Phase: "remuxing",
|
|
||||||
Label: "Remux fehlgeschlagen",
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
out = strings.TrimSpace(newOut)
|
out = strings.TrimSpace(newOut)
|
||||||
jobsMu.Lock()
|
jobsMu.Lock()
|
||||||
job.Output = out
|
job.Output = out
|
||||||
jobsMu.Unlock()
|
jobsMu.Unlock()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2) Move to done
|
// 2) Move to done
|
||||||
setPhase("moving", 10)
|
setPhase("moving", 10)
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
{
|
{
|
||||||
"databaseUrl": "postgres://postgres@127.0.0.1:5432/nsfwapp?sslmode=disable",
|
"databaseUrl": "postgres://postgres@127.0.0.1:5432/nsfwapp?sslmode=disable",
|
||||||
"encryptedDbPassword": "vkNvHM+/AgEkRPW0X+rDDys4bPb5Q7ZPGqPSADCluIJ6GCDGNGlDy3s=",
|
"encryptedDbPassword": "vkNvHM+/AgEkRPW0X+rDDys4bPb5Q7ZPGqPSADCluIJ6GCDGNGlDy3s=",
|
||||||
"recordDir": "C:\\Users\\Chris\\Desktop\\privat\\records",
|
"recordDir": "C:\\Users\\Rother\\Desktop\\Privat\\records",
|
||||||
"doneDir": "C:\\Users\\Chris\\Desktop\\privat\\records\\done",
|
"doneDir": "C:\\Users\\Rother\\Desktop\\Privat\\records\\done",
|
||||||
"ffmpegPath": "",
|
"ffmpegPath": "",
|
||||||
"autoAddToDownloadList": true,
|
"autoAddToDownloadList": true,
|
||||||
"autoStartAddedDownloads": true,
|
"autoStartAddedDownloads": true,
|
||||||
"enableConcurrentDownloadsLimit": true,
|
|
||||||
"maxConcurrentDownloads": 40,
|
|
||||||
"useChaturbateApi": true,
|
"useChaturbateApi": true,
|
||||||
"useMyFreeCamsWatcher": true,
|
"useMyFreeCamsWatcher": true,
|
||||||
"autoDeleteSmallDownloads": false,
|
"autoDeleteSmallDownloads": false,
|
||||||
|
|||||||
@ -66,7 +66,6 @@ func registerRoutes(mux *http.ServeMux, auth *AuthManager) *ModelStore {
|
|||||||
api.HandleFunc("/api/record/regenerate-assets", handleRegenerateAssets)
|
api.HandleFunc("/api/record/regenerate-assets", handleRegenerateAssets)
|
||||||
api.HandleFunc("/api/record/delete-many", handleDeleteMany)
|
api.HandleFunc("/api/record/delete-many", handleDeleteMany)
|
||||||
api.HandleFunc("/api/record/keep-many", handleKeepMany)
|
api.HandleFunc("/api/record/keep-many", handleKeepMany)
|
||||||
api.HandleFunc("/api/record/concurrency", recordConcurrentDownloadsStatus)
|
|
||||||
|
|
||||||
api.HandleFunc("/api/chaturbate/online", chaturbateOnlineHandler)
|
api.HandleFunc("/api/chaturbate/online", chaturbateOnlineHandler)
|
||||||
api.HandleFunc("/api/chaturbate/biocontext", chaturbateBioContextHandler)
|
api.HandleFunc("/api/chaturbate/biocontext", chaturbateBioContextHandler)
|
||||||
@ -183,54 +182,6 @@ func buildPostgresDSNFromSettings() (string, error) {
|
|||||||
return u.String(), nil
|
return u.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildPostgresDSNFromRecorderSettings(s RecorderSettings) (string, error) {
|
|
||||||
dbURL := strings.TrimSpace(s.DatabaseURL)
|
|
||||||
if dbURL == "" {
|
|
||||||
return "", fmt.Errorf("databaseUrl ist leer")
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(dbURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("databaseUrl ungültig: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn URL bereits ein echtes Passwort enthält, direkt verwenden
|
|
||||||
if u.User != nil {
|
|
||||||
if pw, hasPw := u.User.Password(); hasPw {
|
|
||||||
pw = strings.TrimSpace(pw)
|
|
||||||
if pw != "" && pw != "****" {
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := strings.TrimSpace(s.EncryptedDBPassword)
|
|
||||||
if enc == "" {
|
|
||||||
// URL ohne Passwort ist ok
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
plainPw, err := decryptSettingString(enc)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("db password decrypt failed: %w", err)
|
|
||||||
}
|
|
||||||
plainPw = strings.TrimSpace(plainPw)
|
|
||||||
if plainPw == "" {
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user := ""
|
|
||||||
if u.User != nil {
|
|
||||||
user = u.User.Username()
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(user) == "" {
|
|
||||||
return "", fmt.Errorf("databaseUrl enthält keinen Username, kann Passwort nicht einsetzen")
|
|
||||||
}
|
|
||||||
|
|
||||||
u.User = url.UserPassword(user, plainPw)
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitizeDSNForLog entfernt Passwort aus DSN, damit du es gefahrlos loggen kannst.
|
// sanitizeDSNForLog entfernt Passwort aus DSN, damit du es gefahrlos loggen kannst.
|
||||||
func sanitizeDSNForLog(dsn string) string {
|
func sanitizeDSNForLog(dsn string) string {
|
||||||
dsn = strings.TrimSpace(dsn)
|
dsn = strings.TrimSpace(dsn)
|
||||||
|
|||||||
@ -24,8 +24,6 @@ type RecorderSettings struct {
|
|||||||
|
|
||||||
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
|
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
|
||||||
AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"`
|
AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"`
|
||||||
EnableConcurrentDownloadsLimit bool `json:"enableConcurrentDownloadsLimit"`
|
|
||||||
MaxConcurrentDownloads int `json:"maxConcurrentDownloads"`
|
|
||||||
|
|
||||||
UseChaturbateAPI bool `json:"useChaturbateApi"`
|
UseChaturbateAPI bool `json:"useChaturbateApi"`
|
||||||
UseMyFreeCamsWatcher bool `json:"useMyFreeCamsWatcher"`
|
UseMyFreeCamsWatcher bool `json:"useMyFreeCamsWatcher"`
|
||||||
@ -56,8 +54,6 @@ var (
|
|||||||
|
|
||||||
AutoAddToDownloadList: false,
|
AutoAddToDownloadList: false,
|
||||||
AutoStartAddedDownloads: false,
|
AutoStartAddedDownloads: false,
|
||||||
EnableConcurrentDownloadsLimit: false,
|
|
||||||
MaxConcurrentDownloads: 20,
|
|
||||||
|
|
||||||
UseChaturbateAPI: false,
|
UseChaturbateAPI: false,
|
||||||
UseMyFreeCamsWatcher: false,
|
UseMyFreeCamsWatcher: false,
|
||||||
@ -211,8 +207,6 @@ type RecorderSettingsPublic struct {
|
|||||||
|
|
||||||
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
|
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
|
||||||
AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"`
|
AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"`
|
||||||
EnableConcurrentDownloadsLimit bool `json:"enableConcurrentDownloadsLimit"`
|
|
||||||
MaxConcurrentDownloads int `json:"maxConcurrentDownloads"`
|
|
||||||
|
|
||||||
UseChaturbateAPI bool `json:"useChaturbateApi"`
|
UseChaturbateAPI bool `json:"useChaturbateApi"`
|
||||||
UseMyFreeCamsWatcher bool `json:"useMyFreeCamsWatcher"`
|
UseMyFreeCamsWatcher bool `json:"useMyFreeCamsWatcher"`
|
||||||
@ -239,8 +233,6 @@ func toPublicSettings(s RecorderSettings) RecorderSettingsPublic {
|
|||||||
|
|
||||||
AutoAddToDownloadList: s.AutoAddToDownloadList,
|
AutoAddToDownloadList: s.AutoAddToDownloadList,
|
||||||
AutoStartAddedDownloads: s.AutoStartAddedDownloads,
|
AutoStartAddedDownloads: s.AutoStartAddedDownloads,
|
||||||
EnableConcurrentDownloadsLimit: s.EnableConcurrentDownloadsLimit,
|
|
||||||
MaxConcurrentDownloads: s.MaxConcurrentDownloads,
|
|
||||||
|
|
||||||
UseChaturbateAPI: s.UseChaturbateAPI,
|
UseChaturbateAPI: s.UseChaturbateAPI,
|
||||||
UseMyFreeCamsWatcher: s.UseMyFreeCamsWatcher,
|
UseMyFreeCamsWatcher: s.UseMyFreeCamsWatcher,
|
||||||
@ -318,12 +310,6 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if in.LowDiskPauseBelowGB > 10_000 {
|
if in.LowDiskPauseBelowGB > 10_000 {
|
||||||
in.LowDiskPauseBelowGB = 10_000
|
in.LowDiskPauseBelowGB = 10_000
|
||||||
}
|
}
|
||||||
if in.MaxConcurrentDownloads < 1 {
|
|
||||||
in.MaxConcurrentDownloads = 1
|
|
||||||
}
|
|
||||||
if in.MaxConcurrentDownloads > 100 {
|
|
||||||
in.MaxConcurrentDownloads = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ensure folders (Fehler zurückgeben, falls z.B. keine Rechte) ---
|
// --- ensure folders (Fehler zurückgeben, falls z.B. keine Rechte) ---
|
||||||
recAbs, err := resolvePathRelativeToApp(in.RecordDir)
|
recAbs, err := resolvePathRelativeToApp(in.RecordDir)
|
||||||
@ -380,64 +366,32 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
in.EncryptedDBPassword = current.EncryptedDBPassword
|
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.AutoStartAddedDownloads = in.AutoStartAddedDownloads
|
|
||||||
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.LowDiskPauseBelowGB = in.LowDiskPauseBelowGB
|
|
||||||
|
|
||||||
next.BlurPreviews = in.BlurPreviews
|
|
||||||
next.TeaserPlayback = in.TeaserPlayback
|
|
||||||
next.TeaserAudio = in.TeaserAudio
|
|
||||||
|
|
||||||
next.EnableNotifications = in.EnableNotifications
|
|
||||||
|
|
||||||
dbChanged :=
|
dbChanged :=
|
||||||
strings.TrimSpace(next.DatabaseURL) != strings.TrimSpace(current.DatabaseURL) ||
|
strings.TrimSpace(in.DatabaseURL) != strings.TrimSpace(current.DatabaseURL) ||
|
||||||
strings.TrimSpace(next.EncryptedDBPassword) != strings.TrimSpace(current.EncryptedDBPassword)
|
strings.TrimSpace(in.EncryptedDBPassword) != strings.TrimSpace(current.EncryptedDBPassword)
|
||||||
|
|
||||||
var newStore *ModelStore
|
// ✅ Settings im RAM aktualisieren
|
||||||
|
settingsMu.Lock()
|
||||||
|
settings = in.RecorderSettings
|
||||||
|
settingsMu.Unlock()
|
||||||
|
|
||||||
// DB erst prüfen, BEVOR gespeichert wird
|
// ✅ Settings auf Disk persistieren
|
||||||
|
saveSettingsToDisk()
|
||||||
|
|
||||||
|
// ✅ Wenn DB geändert wurde: ModelStore sofort auf neue DB umstellen
|
||||||
if dbChanged {
|
if dbChanged {
|
||||||
dsn, err := buildPostgresDSNFromRecorderSettings(next)
|
dsn, err := buildPostgresDSNFromSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "ungültige Datenbank-Konfiguration: "+err.Error(), http.StatusBadRequest)
|
http.Error(w, "ungültige Datenbank-Konfiguration: "+err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newStore = NewModelStore(dsn)
|
newStore := NewModelStore(dsn)
|
||||||
if err := newStore.Load(); err != nil {
|
if err := newStore.Load(); err != nil {
|
||||||
http.Error(w, "Datenbank-Verbindung fehlgeschlagen: "+err.Error(), http.StatusBadRequest)
|
http.Error(w, "Datenbank-Verbindung fehlgeschlagen: "+err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Erst jetzt wirklich übernehmen
|
|
||||||
settingsMu.Lock()
|
|
||||||
settings = next
|
|
||||||
settingsMu.Unlock()
|
|
||||||
|
|
||||||
saveSettingsToDisk()
|
|
||||||
|
|
||||||
if newStore != nil {
|
|
||||||
setModelStore(newStore)
|
setModelStore(newStore)
|
||||||
setChaturbateOnlineModelStore(newStore)
|
setChaturbateOnlineModelStore(newStore)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,8 +57,6 @@ type RecorderSettingsState = {
|
|||||||
ffmpegPath?: string
|
ffmpegPath?: string
|
||||||
autoAddToDownloadList?: boolean
|
autoAddToDownloadList?: boolean
|
||||||
autoStartAddedDownloads?: boolean
|
autoStartAddedDownloads?: boolean
|
||||||
enableConcurrentDownloadsLimit?: boolean
|
|
||||||
maxConcurrentDownloads?: number
|
|
||||||
useChaturbateApi?: boolean
|
useChaturbateApi?: boolean
|
||||||
useMyFreeCamsWatcher?: boolean
|
useMyFreeCamsWatcher?: boolean
|
||||||
autoDeleteSmallDownloads?: boolean
|
autoDeleteSmallDownloads?: boolean
|
||||||
@ -128,8 +126,6 @@ const DEFAULT_RECORDER_SETTINGS: RecorderSettingsState = {
|
|||||||
ffmpegPath: '',
|
ffmpegPath: '',
|
||||||
autoAddToDownloadList: false,
|
autoAddToDownloadList: false,
|
||||||
autoStartAddedDownloads: false,
|
autoStartAddedDownloads: false,
|
||||||
enableConcurrentDownloadsLimit: false,
|
|
||||||
maxConcurrentDownloads: 20,
|
|
||||||
useChaturbateApi: false,
|
useChaturbateApi: false,
|
||||||
useMyFreeCamsWatcher: false,
|
useMyFreeCamsWatcher: false,
|
||||||
autoDeleteSmallDownloads: false,
|
autoDeleteSmallDownloads: false,
|
||||||
@ -3566,8 +3562,6 @@ export default function App() {
|
|||||||
onAddToDownloads={handleAddToDownloads}
|
onAddToDownloads={handleAddToDownloads}
|
||||||
onRemovePending={handleRemovePendingWatchedRoom}
|
onRemovePending={handleRemovePendingWatchedRoom}
|
||||||
blurPreviews={Boolean(recSettings.blurPreviews)}
|
blurPreviews={Boolean(recSettings.blurPreviews)}
|
||||||
enableConcurrentDownloadsLimit={Boolean(recSettings.enableConcurrentDownloadsLimit)}
|
|
||||||
maxConcurrentDownloads={Number(recSettings.maxConcurrentDownloads ?? 20)}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@ -48,9 +48,6 @@ type Props = {
|
|||||||
onToggleWatch?: (job: RecordJob) => void | Promise<void>
|
onToggleWatch?: (job: RecordJob) => void | Promise<void>
|
||||||
onAddToDownloads?: (job: RecordJob) => boolean | void | Promise<boolean | void>
|
onAddToDownloads?: (job: RecordJob) => boolean | void | Promise<boolean | void>
|
||||||
onRemovePending?: (pending: PendingWatchedRoom) => void | Promise<void>
|
onRemovePending?: (pending: PendingWatchedRoom) => void | Promise<void>
|
||||||
|
|
||||||
enableConcurrentDownloadsLimit?: boolean
|
|
||||||
maxConcurrentDownloads?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingModelName = (p: PendingWatchedRoom) => {
|
const pendingModelName = (p: PendingWatchedRoom) => {
|
||||||
@ -568,8 +565,6 @@ export default function Downloads({
|
|||||||
modelsByKey = {},
|
modelsByKey = {},
|
||||||
roomStatusByModelKey = {},
|
roomStatusByModelKey = {},
|
||||||
blurPreviews,
|
blurPreviews,
|
||||||
enableConcurrentDownloadsLimit = false,
|
|
||||||
maxConcurrentDownloads = 20,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const isDesktop = useMediaQuery('(min-width: 640px)', true)
|
const isDesktop = useMediaQuery('(min-width: 640px)', true)
|
||||||
|
|
||||||
@ -769,18 +764,6 @@ export default function Downloads({
|
|||||||
return jobs.some((j) => !j.endedAt && j.status === 'running')
|
return jobs.some((j) => !j.endedAt && j.status === 'running')
|
||||||
}, [jobs])
|
}, [jobs])
|
||||||
|
|
||||||
const activeDownloadCount = useMemo(() => {
|
|
||||||
return jobs.filter((j) => {
|
|
||||||
if (isPostworkJob(j)) return false
|
|
||||||
if ((j as any).endedAt) return false
|
|
||||||
return String(j.status ?? '').trim().toLowerCase() === 'running'
|
|
||||||
}).length
|
|
||||||
}, [jobs])
|
|
||||||
|
|
||||||
const concurrentLimitHintVisible = enableConcurrentDownloadsLimit
|
|
||||||
const concurrentLimitReached =
|
|
||||||
enableConcurrentDownloadsLimit && activeDownloadCount >= maxConcurrentDownloads
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGrowingByJobId((prev) => {
|
setGrowingByJobId((prev) => {
|
||||||
const next: Record<string, boolean> = { ...prev }
|
const next: Record<string, boolean> = { ...prev }
|
||||||
@ -1398,26 +1381,7 @@ export default function Downloads({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shrink-0 flex items-center gap-2 min-w-0">
|
<div className="shrink-0 flex items-center gap-2">
|
||||||
{concurrentLimitHintVisible ? (
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
'flex items-center rounded-full px-2.5 py-1 text-xs font-medium ring-1 whitespace-nowrap',
|
|
||||||
concurrentLimitReached
|
|
||||||
? 'bg-amber-50 text-amber-900 ring-amber-200 dark:bg-amber-400/10 dark:text-amber-100 dark:ring-amber-400/20'
|
|
||||||
: 'bg-blue-50 text-blue-900 ring-blue-200 dark:bg-blue-400/10 dark:text-blue-100 dark:ring-blue-400/20',
|
|
||||||
].join(' ')}
|
|
||||||
title={
|
|
||||||
concurrentLimitReached
|
|
||||||
? 'Maximale Anzahl gleichzeitiger Downloads erreicht'
|
|
||||||
: 'Begrenzung für gleichzeitige Downloads ist aktiv'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Downloads: {activeDownloadCount}/{maxConcurrentDownloads}
|
|
||||||
{concurrentLimitReached ? ' · Limit erreicht' : ''}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={watchedPaused ? 'secondary' : 'primary'}
|
variant={watchedPaused ? 'secondary' : 'primary'}
|
||||||
|
|||||||
@ -19,8 +19,6 @@ type RecorderSettings = {
|
|||||||
ffmpegPath?: string
|
ffmpegPath?: string
|
||||||
autoAddToDownloadList?: boolean
|
autoAddToDownloadList?: boolean
|
||||||
autoStartAddedDownloads?: boolean
|
autoStartAddedDownloads?: boolean
|
||||||
enableConcurrentDownloadsLimit?: boolean
|
|
||||||
maxConcurrentDownloads?: number
|
|
||||||
useChaturbateApi?: boolean
|
useChaturbateApi?: boolean
|
||||||
useMyFreeCamsWatcher?: boolean
|
useMyFreeCamsWatcher?: boolean
|
||||||
autoDeleteSmallDownloads?: boolean
|
autoDeleteSmallDownloads?: boolean
|
||||||
@ -49,8 +47,6 @@ const DEFAULTS: RecorderSettings = {
|
|||||||
doneDir: 'records/done',
|
doneDir: 'records/done',
|
||||||
ffmpegPath: '',
|
ffmpegPath: '',
|
||||||
autoAddToDownloadList: true,
|
autoAddToDownloadList: true,
|
||||||
enableConcurrentDownloadsLimit: false,
|
|
||||||
maxConcurrentDownloads: 20,
|
|
||||||
useChaturbateApi: false,
|
useChaturbateApi: false,
|
||||||
useMyFreeCamsWatcher: false,
|
useMyFreeCamsWatcher: false,
|
||||||
autoDeleteSmallDownloads: true,
|
autoDeleteSmallDownloads: true,
|
||||||
@ -189,10 +185,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
// ✅ falls backend die Felder noch nicht hat -> defaults nutzen
|
// ✅ falls backend die Felder noch nicht hat -> defaults nutzen
|
||||||
autoAddToDownloadList: data.autoAddToDownloadList ?? DEFAULTS.autoAddToDownloadList,
|
autoAddToDownloadList: data.autoAddToDownloadList ?? DEFAULTS.autoAddToDownloadList,
|
||||||
autoStartAddedDownloads: data.autoStartAddedDownloads ?? DEFAULTS.autoStartAddedDownloads,
|
autoStartAddedDownloads: data.autoStartAddedDownloads ?? DEFAULTS.autoStartAddedDownloads,
|
||||||
enableConcurrentDownloadsLimit:
|
|
||||||
(data as any).enableConcurrentDownloadsLimit ?? DEFAULTS.enableConcurrentDownloadsLimit,
|
|
||||||
maxConcurrentDownloads:
|
|
||||||
(data as any).maxConcurrentDownloads ?? DEFAULTS.maxConcurrentDownloads,
|
|
||||||
|
|
||||||
useChaturbateApi: data.useChaturbateApi ?? DEFAULTS.useChaturbateApi,
|
useChaturbateApi: data.useChaturbateApi ?? DEFAULTS.useChaturbateApi,
|
||||||
useMyFreeCamsWatcher: data.useMyFreeCamsWatcher ?? DEFAULTS.useMyFreeCamsWatcher,
|
useMyFreeCamsWatcher: data.useMyFreeCamsWatcher ?? DEFAULTS.useMyFreeCamsWatcher,
|
||||||
@ -523,11 +515,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
// ✅ Switch-Logik: Autostart nur sinnvoll, wenn Auto-Add aktiv ist
|
// ✅ Switch-Logik: Autostart nur sinnvoll, wenn Auto-Add aktiv ist
|
||||||
const autoAddToDownloadList = !!value.autoAddToDownloadList
|
const autoAddToDownloadList = !!value.autoAddToDownloadList
|
||||||
const autoStartAddedDownloads = autoAddToDownloadList ? !!value.autoStartAddedDownloads : false
|
const autoStartAddedDownloads = autoAddToDownloadList ? !!value.autoStartAddedDownloads : false
|
||||||
const enableConcurrentDownloadsLimit = !!(value as any).enableConcurrentDownloadsLimit
|
|
||||||
const maxConcurrentDownloads = Math.max(
|
|
||||||
1,
|
|
||||||
Math.min(100, Math.floor(Number((value as any).maxConcurrentDownloads ?? DEFAULTS.maxConcurrentDownloads ?? 20)))
|
|
||||||
)
|
|
||||||
|
|
||||||
const useChaturbateApi = !!value.useChaturbateApi
|
const useChaturbateApi = !!value.useChaturbateApi
|
||||||
const useMyFreeCamsWatcher = !!value.useMyFreeCamsWatcher
|
const useMyFreeCamsWatcher = !!value.useMyFreeCamsWatcher
|
||||||
@ -558,8 +545,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
ffmpegPath,
|
ffmpegPath,
|
||||||
autoAddToDownloadList,
|
autoAddToDownloadList,
|
||||||
autoStartAddedDownloads,
|
autoStartAddedDownloads,
|
||||||
enableConcurrentDownloadsLimit,
|
|
||||||
maxConcurrentDownloads,
|
|
||||||
useChaturbateApi,
|
useChaturbateApi,
|
||||||
useMyFreeCamsWatcher,
|
useMyFreeCamsWatcher,
|
||||||
autoDeleteSmallDownloads,
|
autoDeleteSmallDownloads,
|
||||||
@ -1056,54 +1041,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
|
|
||||||
<LabeledSwitch
|
|
||||||
checked={!!value.enableConcurrentDownloadsLimit}
|
|
||||||
onChange={(checked) =>
|
|
||||||
setValue((v) => ({
|
|
||||||
...v,
|
|
||||||
enableConcurrentDownloadsLimit: checked,
|
|
||||||
maxConcurrentDownloads: v.maxConcurrentDownloads ?? 20,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
label="Gleichzeitige Downloads begrenzen"
|
|
||||||
description="Begrenzt die Anzahl parallel laufender Downloads. Weitere Starts werden vom Backend blockiert."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'mt-2 grid grid-cols-1 gap-2 sm:grid-cols-12 sm:items-center ' +
|
|
||||||
(!value.enableConcurrentDownloadsLimit ? 'opacity-50 pointer-events-none' : '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="sm:col-span-4">
|
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-200">Max. parallele Downloads</div>
|
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-300">
|
|
||||||
Mehr gleichzeitige Starts werden nicht zugelassen.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="sm:col-span-8">
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
value={value.maxConcurrentDownloads ?? 20}
|
|
||||||
onChange={(e) =>
|
|
||||||
setValue((v) => ({
|
|
||||||
...v,
|
|
||||||
maxConcurrentDownloads: Number(e.target.value || 1),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="h-9 w-32 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm
|
|
||||||
dark:border-white/10 dark:bg-gray-900 dark:text-gray-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<LabeledSwitch
|
<LabeledSwitch
|
||||||
checked={!!value.autoAddToDownloadList}
|
checked={!!value.autoAddToDownloadList}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user