updated
This commit is contained in:
parent
c5739f7d44
commit
548919cde2
@ -96,6 +96,28 @@ type bulkFilesRequest struct {
|
||||
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) {
|
||||
b, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
|
||||
@ -254,6 +254,48 @@ func recordPreviewSprite(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ---------------- 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) {
|
||||
url := strings.TrimSpace(req.URL)
|
||||
if url == "" {
|
||||
@ -261,19 +303,39 @@ func startRecordingInternal(req RecordRequest) (*RecordJob, error) {
|
||||
}
|
||||
|
||||
jobsMu.Lock()
|
||||
defer jobsMu.Unlock()
|
||||
|
||||
// Duplicate-Check
|
||||
for _, j := range jobs {
|
||||
if j != nil && j.Status == JobRunning && j.EndedAt == nil && strings.TrimSpace(j.SourceURL) == url {
|
||||
if j.Hidden && !req.Hidden {
|
||||
j.Hidden = false
|
||||
jobsMu.Unlock()
|
||||
return j, nil
|
||||
}
|
||||
|
||||
jobsMu.Unlock()
|
||||
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()
|
||||
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"))
|
||||
|
||||
s := getSettings()
|
||||
recordDirAbs, _ := resolvePathRelativeToApp(s.RecordDir)
|
||||
recordDir := strings.TrimSpace(recordDirAbs)
|
||||
if recordDir == "" {
|
||||
@ -313,7 +374,6 @@ func startRecordingInternal(req RecordRequest) (*RecordJob, error) {
|
||||
}
|
||||
|
||||
jobs[jobID] = job
|
||||
jobsMu.Unlock()
|
||||
|
||||
go runJob(ctx, job, req)
|
||||
return job, nil
|
||||
@ -608,16 +668,47 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
})
|
||||
}
|
||||
|
||||
// 1) Remux
|
||||
// 1) Remux
|
||||
if strings.EqualFold(filepath.Ext(out), ".ts") {
|
||||
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)
|
||||
jobsMu.Lock()
|
||||
job.Output = out
|
||||
jobsMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Move to done
|
||||
setPhase("moving", 10)
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
{
|
||||
"databaseUrl": "postgres://postgres@127.0.0.1:5432/nsfwapp?sslmode=disable",
|
||||
"encryptedDbPassword": "vkNvHM+/AgEkRPW0X+rDDys4bPb5Q7ZPGqPSADCluIJ6GCDGNGlDy3s=",
|
||||
"recordDir": "C:\\Users\\Rother\\Desktop\\Privat\\records",
|
||||
"doneDir": "C:\\Users\\Rother\\Desktop\\Privat\\records\\done",
|
||||
"recordDir": "C:\\Users\\Chris\\Desktop\\privat\\records",
|
||||
"doneDir": "C:\\Users\\Chris\\Desktop\\privat\\records\\done",
|
||||
"ffmpegPath": "",
|
||||
"autoAddToDownloadList": true,
|
||||
"autoStartAddedDownloads": true,
|
||||
"enableConcurrentDownloadsLimit": true,
|
||||
"maxConcurrentDownloads": 40,
|
||||
"useChaturbateApi": true,
|
||||
"useMyFreeCamsWatcher": true,
|
||||
"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/delete-many", handleDeleteMany)
|
||||
api.HandleFunc("/api/record/keep-many", handleKeepMany)
|
||||
api.HandleFunc("/api/record/concurrency", recordConcurrentDownloadsStatus)
|
||||
|
||||
api.HandleFunc("/api/chaturbate/online", chaturbateOnlineHandler)
|
||||
api.HandleFunc("/api/chaturbate/biocontext", chaturbateBioContextHandler)
|
||||
@ -182,6 +183,54 @@ func buildPostgresDSNFromSettings() (string, error) {
|
||||
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.
|
||||
func sanitizeDSNForLog(dsn string) string {
|
||||
dsn = strings.TrimSpace(dsn)
|
||||
|
||||
@ -24,6 +24,8 @@ type RecorderSettings struct {
|
||||
|
||||
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
|
||||
AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"`
|
||||
EnableConcurrentDownloadsLimit bool `json:"enableConcurrentDownloadsLimit"`
|
||||
MaxConcurrentDownloads int `json:"maxConcurrentDownloads"`
|
||||
|
||||
UseChaturbateAPI bool `json:"useChaturbateApi"`
|
||||
UseMyFreeCamsWatcher bool `json:"useMyFreeCamsWatcher"`
|
||||
@ -54,6 +56,8 @@ var (
|
||||
|
||||
AutoAddToDownloadList: false,
|
||||
AutoStartAddedDownloads: false,
|
||||
EnableConcurrentDownloadsLimit: false,
|
||||
MaxConcurrentDownloads: 20,
|
||||
|
||||
UseChaturbateAPI: false,
|
||||
UseMyFreeCamsWatcher: false,
|
||||
@ -207,6 +211,8 @@ type RecorderSettingsPublic struct {
|
||||
|
||||
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
|
||||
AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"`
|
||||
EnableConcurrentDownloadsLimit bool `json:"enableConcurrentDownloadsLimit"`
|
||||
MaxConcurrentDownloads int `json:"maxConcurrentDownloads"`
|
||||
|
||||
UseChaturbateAPI bool `json:"useChaturbateApi"`
|
||||
UseMyFreeCamsWatcher bool `json:"useMyFreeCamsWatcher"`
|
||||
@ -233,6 +239,8 @@ func toPublicSettings(s RecorderSettings) RecorderSettingsPublic {
|
||||
|
||||
AutoAddToDownloadList: s.AutoAddToDownloadList,
|
||||
AutoStartAddedDownloads: s.AutoStartAddedDownloads,
|
||||
EnableConcurrentDownloadsLimit: s.EnableConcurrentDownloadsLimit,
|
||||
MaxConcurrentDownloads: s.MaxConcurrentDownloads,
|
||||
|
||||
UseChaturbateAPI: s.UseChaturbateAPI,
|
||||
UseMyFreeCamsWatcher: s.UseMyFreeCamsWatcher,
|
||||
@ -310,6 +318,12 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if 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) ---
|
||||
recAbs, err := resolvePathRelativeToApp(in.RecordDir)
|
||||
@ -366,32 +380,64 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
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 :=
|
||||
strings.TrimSpace(in.DatabaseURL) != strings.TrimSpace(current.DatabaseURL) ||
|
||||
strings.TrimSpace(in.EncryptedDBPassword) != strings.TrimSpace(current.EncryptedDBPassword)
|
||||
strings.TrimSpace(next.DatabaseURL) != strings.TrimSpace(current.DatabaseURL) ||
|
||||
strings.TrimSpace(next.EncryptedDBPassword) != strings.TrimSpace(current.EncryptedDBPassword)
|
||||
|
||||
// ✅ Settings im RAM aktualisieren
|
||||
settingsMu.Lock()
|
||||
settings = in.RecorderSettings
|
||||
settingsMu.Unlock()
|
||||
var newStore *ModelStore
|
||||
|
||||
// ✅ Settings auf Disk persistieren
|
||||
saveSettingsToDisk()
|
||||
|
||||
// ✅ Wenn DB geändert wurde: ModelStore sofort auf neue DB umstellen
|
||||
// DB erst prüfen, BEVOR gespeichert wird
|
||||
if dbChanged {
|
||||
dsn, err := buildPostgresDSNFromSettings()
|
||||
dsn, err := buildPostgresDSNFromRecorderSettings(next)
|
||||
if err != nil {
|
||||
http.Error(w, "ungültige Datenbank-Konfiguration: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
newStore := NewModelStore(dsn)
|
||||
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 newStore != nil {
|
||||
setModelStore(newStore)
|
||||
setChaturbateOnlineModelStore(newStore)
|
||||
}
|
||||
|
||||
@ -57,6 +57,8 @@ type RecorderSettingsState = {
|
||||
ffmpegPath?: string
|
||||
autoAddToDownloadList?: boolean
|
||||
autoStartAddedDownloads?: boolean
|
||||
enableConcurrentDownloadsLimit?: boolean
|
||||
maxConcurrentDownloads?: number
|
||||
useChaturbateApi?: boolean
|
||||
useMyFreeCamsWatcher?: boolean
|
||||
autoDeleteSmallDownloads?: boolean
|
||||
@ -126,6 +128,8 @@ const DEFAULT_RECORDER_SETTINGS: RecorderSettingsState = {
|
||||
ffmpegPath: '',
|
||||
autoAddToDownloadList: false,
|
||||
autoStartAddedDownloads: false,
|
||||
enableConcurrentDownloadsLimit: false,
|
||||
maxConcurrentDownloads: 20,
|
||||
useChaturbateApi: false,
|
||||
useMyFreeCamsWatcher: false,
|
||||
autoDeleteSmallDownloads: false,
|
||||
@ -3562,6 +3566,8 @@ export default function App() {
|
||||
onAddToDownloads={handleAddToDownloads}
|
||||
onRemovePending={handleRemovePendingWatchedRoom}
|
||||
blurPreviews={Boolean(recSettings.blurPreviews)}
|
||||
enableConcurrentDownloadsLimit={Boolean(recSettings.enableConcurrentDownloadsLimit)}
|
||||
maxConcurrentDownloads={Number(recSettings.maxConcurrentDownloads ?? 20)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
@ -48,6 +48,9 @@ type Props = {
|
||||
onToggleWatch?: (job: RecordJob) => void | Promise<void>
|
||||
onAddToDownloads?: (job: RecordJob) => boolean | void | Promise<boolean | void>
|
||||
onRemovePending?: (pending: PendingWatchedRoom) => void | Promise<void>
|
||||
|
||||
enableConcurrentDownloadsLimit?: boolean
|
||||
maxConcurrentDownloads?: number
|
||||
}
|
||||
|
||||
const pendingModelName = (p: PendingWatchedRoom) => {
|
||||
@ -565,6 +568,8 @@ export default function Downloads({
|
||||
modelsByKey = {},
|
||||
roomStatusByModelKey = {},
|
||||
blurPreviews,
|
||||
enableConcurrentDownloadsLimit = false,
|
||||
maxConcurrentDownloads = 20,
|
||||
}: Props) {
|
||||
const isDesktop = useMediaQuery('(min-width: 640px)', true)
|
||||
|
||||
@ -764,6 +769,18 @@ export default function Downloads({
|
||||
return jobs.some((j) => !j.endedAt && j.status === 'running')
|
||||
}, [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(() => {
|
||||
setGrowingByJobId((prev) => {
|
||||
const next: Record<string, boolean> = { ...prev }
|
||||
@ -1381,7 +1398,26 @@ export default function Downloads({
|
||||
</span>
|
||||
</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
|
||||
size="sm"
|
||||
variant={watchedPaused ? 'secondary' : 'primary'}
|
||||
|
||||
@ -19,6 +19,8 @@ type RecorderSettings = {
|
||||
ffmpegPath?: string
|
||||
autoAddToDownloadList?: boolean
|
||||
autoStartAddedDownloads?: boolean
|
||||
enableConcurrentDownloadsLimit?: boolean
|
||||
maxConcurrentDownloads?: number
|
||||
useChaturbateApi?: boolean
|
||||
useMyFreeCamsWatcher?: boolean
|
||||
autoDeleteSmallDownloads?: boolean
|
||||
@ -47,6 +49,8 @@ const DEFAULTS: RecorderSettings = {
|
||||
doneDir: 'records/done',
|
||||
ffmpegPath: '',
|
||||
autoAddToDownloadList: true,
|
||||
enableConcurrentDownloadsLimit: false,
|
||||
maxConcurrentDownloads: 20,
|
||||
useChaturbateApi: false,
|
||||
useMyFreeCamsWatcher: false,
|
||||
autoDeleteSmallDownloads: true,
|
||||
@ -185,6 +189,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
// ✅ falls backend die Felder noch nicht hat -> defaults nutzen
|
||||
autoAddToDownloadList: data.autoAddToDownloadList ?? DEFAULTS.autoAddToDownloadList,
|
||||
autoStartAddedDownloads: data.autoStartAddedDownloads ?? DEFAULTS.autoStartAddedDownloads,
|
||||
enableConcurrentDownloadsLimit:
|
||||
(data as any).enableConcurrentDownloadsLimit ?? DEFAULTS.enableConcurrentDownloadsLimit,
|
||||
maxConcurrentDownloads:
|
||||
(data as any).maxConcurrentDownloads ?? DEFAULTS.maxConcurrentDownloads,
|
||||
|
||||
useChaturbateApi: data.useChaturbateApi ?? DEFAULTS.useChaturbateApi,
|
||||
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
|
||||
const autoAddToDownloadList = !!value.autoAddToDownloadList
|
||||
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 useMyFreeCamsWatcher = !!value.useMyFreeCamsWatcher
|
||||
@ -545,6 +558,8 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
ffmpegPath,
|
||||
autoAddToDownloadList,
|
||||
autoStartAddedDownloads,
|
||||
enableConcurrentDownloadsLimit,
|
||||
maxConcurrentDownloads,
|
||||
useChaturbateApi,
|
||||
useMyFreeCamsWatcher,
|
||||
autoDeleteSmallDownloads,
|
||||
@ -1041,6 +1056,54 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
</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">
|
||||
<LabeledSwitch
|
||||
checked={!!value.autoAddToDownloadList}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user