updated
This commit is contained in:
parent
c5739f7d44
commit
548919cde2
@ -96,6 +96,28 @@ 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,6 +254,48 @@ 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 == "" {
|
||||||
@ -261,19 +303,39 @@ 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)
|
||||||
|
|
||||||
@ -290,7 +352,6 @@ 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 == "" {
|
||||||
@ -313,7 +374,6 @@ 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
|
||||||
@ -608,15 +668,46 @@ 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) != "" {
|
|
||||||
out = strings.TrimSpace(newOut)
|
newOut, err2 := maybeRemuxTSForJob(job, out)
|
||||||
|
if err2 != nil || strings.TrimSpace(newOut) == "" {
|
||||||
|
finalFile := filepath.Base(out)
|
||||||
|
finalAssetID := assetIDFromVideoPath(out)
|
||||||
|
|
||||||
jobsMu.Lock()
|
jobsMu.Lock()
|
||||||
job.Output = out
|
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()
|
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)
|
||||||
|
jobsMu.Lock()
|
||||||
|
job.Output = out
|
||||||
|
jobsMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Move to done
|
// 2) Move to done
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"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\\Rother\\Desktop\\Privat\\records",
|
"recordDir": "C:\\Users\\Chris\\Desktop\\privat\\records",
|
||||||
"doneDir": "C:\\Users\\Rother\\Desktop\\Privat\\records\\done",
|
"doneDir": "C:\\Users\\Chris\\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,6 +66,7 @@ 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)
|
||||||
@ -182,6 +183,54 @@ 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)
|
||||||
|
|||||||
@ -22,8 +22,10 @@ type RecorderSettings struct {
|
|||||||
DoneDir string `json:"doneDir"`
|
DoneDir string `json:"doneDir"`
|
||||||
FFmpegPath string `json:"ffmpegPath"`
|
FFmpegPath string `json:"ffmpegPath"`
|
||||||
|
|
||||||
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"`
|
||||||
@ -52,8 +54,10 @@ var (
|
|||||||
DoneDir: "/records/done",
|
DoneDir: "/records/done",
|
||||||
FFmpegPath: "",
|
FFmpegPath: "",
|
||||||
|
|
||||||
AutoAddToDownloadList: false,
|
AutoAddToDownloadList: false,
|
||||||
AutoStartAddedDownloads: false,
|
AutoStartAddedDownloads: false,
|
||||||
|
EnableConcurrentDownloadsLimit: false,
|
||||||
|
MaxConcurrentDownloads: 20,
|
||||||
|
|
||||||
UseChaturbateAPI: false,
|
UseChaturbateAPI: false,
|
||||||
UseMyFreeCamsWatcher: false,
|
UseMyFreeCamsWatcher: false,
|
||||||
@ -205,8 +209,10 @@ type RecorderSettingsPublic struct {
|
|||||||
DatabaseURL string `json:"databaseUrl"`
|
DatabaseURL string `json:"databaseUrl"`
|
||||||
HasDBPassword bool `json:"hasDbPassword"`
|
HasDBPassword bool `json:"hasDbPassword"`
|
||||||
|
|
||||||
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"`
|
||||||
@ -231,8 +237,10 @@ func toPublicSettings(s RecorderSettings) RecorderSettingsPublic {
|
|||||||
DatabaseURL: strings.TrimSpace(s.DatabaseURL),
|
DatabaseURL: strings.TrimSpace(s.DatabaseURL),
|
||||||
HasDBPassword: strings.TrimSpace(s.EncryptedDBPassword) != "",
|
HasDBPassword: strings.TrimSpace(s.EncryptedDBPassword) != "",
|
||||||
|
|
||||||
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,
|
||||||
@ -310,6 +318,12 @@ 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)
|
||||||
@ -366,32 +380,64 @@ 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(in.DatabaseURL) != strings.TrimSpace(current.DatabaseURL) ||
|
strings.TrimSpace(next.DatabaseURL) != strings.TrimSpace(current.DatabaseURL) ||
|
||||||
strings.TrimSpace(in.EncryptedDBPassword) != strings.TrimSpace(current.EncryptedDBPassword)
|
strings.TrimSpace(next.EncryptedDBPassword) != strings.TrimSpace(current.EncryptedDBPassword)
|
||||||
|
|
||||||
// ✅ Settings im RAM aktualisieren
|
var newStore *ModelStore
|
||||||
settingsMu.Lock()
|
|
||||||
settings = in.RecorderSettings
|
|
||||||
settingsMu.Unlock()
|
|
||||||
|
|
||||||
// ✅ Settings auf Disk persistieren
|
// DB erst prüfen, BEVOR gespeichert wird
|
||||||
saveSettingsToDisk()
|
|
||||||
|
|
||||||
// ✅ Wenn DB geändert wurde: ModelStore sofort auf neue DB umstellen
|
|
||||||
if dbChanged {
|
if dbChanged {
|
||||||
dsn, err := buildPostgresDSNFromSettings()
|
dsn, err := buildPostgresDSNFromRecorderSettings(next)
|
||||||
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,6 +57,8 @@ 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
|
||||||
@ -126,6 +128,8 @@ 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,
|
||||||
@ -3562,6 +3566,8 @@ 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,6 +48,9 @@ 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) => {
|
||||||
@ -565,6 +568,8 @@ 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)
|
||||||
|
|
||||||
@ -764,6 +769,18 @@ 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 }
|
||||||
@ -1381,7 +1398,26 @@ export default function Downloads({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shrink-0 flex items-center gap-2">
|
<div className="shrink-0 flex items-center gap-2 min-w-0">
|
||||||
|
{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,6 +19,8 @@ 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
|
||||||
@ -47,6 +49,8 @@ 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,
|
||||||
@ -185,6 +189,10 @@ 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,
|
||||||
@ -515,6 +523,11 @@ 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
|
||||||
@ -545,6 +558,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
ffmpegPath,
|
ffmpegPath,
|
||||||
autoAddToDownloadList,
|
autoAddToDownloadList,
|
||||||
autoStartAddedDownloads,
|
autoStartAddedDownloads,
|
||||||
|
enableConcurrentDownloadsLimit,
|
||||||
|
maxConcurrentDownloads,
|
||||||
useChaturbateApi,
|
useChaturbateApi,
|
||||||
useMyFreeCamsWatcher,
|
useMyFreeCamsWatcher,
|
||||||
autoDeleteSmallDownloads,
|
autoDeleteSmallDownloads,
|
||||||
@ -1041,6 +1056,54 @@ 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