This commit is contained in:
Linrador 2026-04-04 09:30:24 +02:00
parent 32bd7c1e6e
commit ce92719ae7
7 changed files with 395 additions and 149 deletions

View File

@ -115,9 +115,11 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
}
s := getSettings()
// ✅ Autostart nur wenn Feature aktiviert ist
// (optional zusätzlich AutoAddToDownloadList wie im Frontend logisch gekoppelt)
if !s.UseChaturbateAPI || !s.AutoStartAddedDownloads || !s.AutoAddToDownloadList {
// Backend-Autostart läuft, wenn
// - Chaturbate API aktiviert ist
// - Autostart NICHT pausiert ist (wird oben schon via isAutostartPaused() geprüft)
if !s.UseChaturbateAPI {
queue = queue[:0]
queued = map[string]bool{}
time.Sleep(2 * time.Second)
@ -145,7 +147,7 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
running := map[string]bool{}
jobsMu.RLock()
for _, j := range jobs {
if j == nil || j.Status != JobRunning {
if !isActiveRecordingJob(j) {
continue
}
u := chaturbateUserFromURL(j.SourceURL)
@ -173,9 +175,6 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
if !ok {
continue
}
if showByUser[it.userKey] == "" {
continue
}
if running[it.userKey] {
continue
}
@ -183,6 +182,8 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
if it.url == "" {
continue
}
// Queue behalten, auch wenn der Snapshot in diesem Tick leer/unvollständig ist
nextQueue = append(nextQueue, it)
nextQueued[it.userKey] = true
}

View File

@ -135,6 +135,118 @@ type importResult struct {
Skipped int `json:"skipped"`
}
type modelsOverviewRequest struct {
Keys []string `json:"keys"`
IncludeWatched bool `json:"includeWatched"`
}
type modelsOverviewModel struct {
ID string `json:"id"`
Input string `json:"input"`
Host string `json:"host,omitempty"`
ModelKey string `json:"modelKey"`
Watching bool `json:"watching"`
Favorite bool `json:"favorite,omitempty"`
Liked *bool `json:"liked,omitempty"`
IsURL bool `json:"isUrl,omitempty"`
Path string `json:"path,omitempty"`
RoomStatus string `json:"roomStatus,omitempty"`
IsOnline bool `json:"isOnline,omitempty"`
ChatRoomURL string `json:"chatRoomUrl,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
LastOnlineAt string `json:"lastOnlineAt,omitempty"`
LastOfflineAt string `json:"lastOfflineAt,omitempty"`
LastRoomSyncAt string `json:"lastRoomSyncAt,omitempty"`
}
type modelsOverviewResponse struct {
TotalCount int `json:"totalCount"`
OnlineModelsCount int `json:"onlineModelsCount"`
OnlineWatchedModelsCount int `json:"onlineWatchedModelsCount"`
OnlineFavCount int `json:"onlineFavCount"`
OnlineLikedCount int `json:"onlineLikedCount"`
Models []modelsOverviewModel `json:"models"`
}
func normalizeModelKeySet(in []string) map[string]bool {
out := make(map[string]bool, len(in))
for _, s := range in {
k := strings.ToLower(strings.TrimSpace(s))
if k == "" {
continue
}
out[k] = true
}
return out
}
func isOnlineRoomStatus(status string, isOnline bool) bool {
s := strings.ToLower(strings.TrimSpace(status))
if s == "offline" {
return false
}
if isOnline {
return true
}
return s != ""
}
func buildModelsOverview(rawList any, req modelsOverviewRequest) (modelsOverviewResponse, error) {
var list []modelsOverviewModel
// absichtlich per JSON-Roundtrip:
// dadurch müssen wir den exakten Store-Typ hier nicht kennen
b, err := json.Marshal(rawList)
if err != nil {
return modelsOverviewResponse{}, err
}
if err := json.Unmarshal(b, &list); err != nil {
return modelsOverviewResponse{}, err
}
keySet := normalizeModelKeySet(req.Keys)
resp := modelsOverviewResponse{
TotalCount: len(list),
Models: make([]modelsOverviewModel, 0, len(keySet)+32),
}
for _, m := range list {
key := strings.ToLower(strings.TrimSpace(m.ModelKey))
if key == "" {
continue
}
online := isOnlineRoomStatus(m.RoomStatus, m.IsOnline)
if strings.Contains(strings.ToLower(m.Host), "chaturbate") {
if online {
resp.OnlineModelsCount++
}
if m.Watching && online {
resp.OnlineWatchedModelsCount++
}
}
if m.Favorite && online {
resp.OnlineFavCount++
}
if m.Liked != nil && *m.Liked && online {
resp.OnlineLikedCount++
}
needModel :=
keySet[key] ||
(req.IncludeWatched && m.Watching)
if needModel {
resp.Models = append(resp.Models, m)
}
}
return resp, nil
}
func importModelsCSV(store *ModelStore, r io.Reader, kind string) (importResult, error) {
cr := csv.NewReader(r)
cr.Comma = ';'
@ -293,6 +405,35 @@ func RegisterModelAPI(mux *http.ServeMux, store *ModelStore) {
modelsWriteJSON(w, http.StatusOK, store.ListWatchedLite(host))
})
mux.HandleFunc("/api/models/overview", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
var req modelsOverviewRequest
if err := modelsReadJSON(r, &req); err != nil {
modelsWriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
store := getModelStore()
if store == nil {
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
return
}
list := store.List()
resp, err := buildModelsOverview(list, req)
if err != nil {
modelsWriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
modelsWriteJSON(w, http.StatusOK, resp)
})
mux.HandleFunc("/api/models", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})

View File

@ -5,7 +5,6 @@
"doneDir": "C:\\Users\\Rother\\Desktop\\Privat\\records\\done",
"ffmpegPath": "",
"autoAddToDownloadList": true,
"autoStartAddedDownloads": true,
"enableConcurrentDownloadsLimit": true,
"maxConcurrentDownloads": 80,
"useChaturbateApi": true,

View File

@ -23,7 +23,6 @@ type RecorderSettings struct {
FFmpegPath string `json:"ffmpegPath"`
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"`
EnableConcurrentDownloadsLimit bool `json:"enableConcurrentDownloadsLimit"`
MaxConcurrentDownloads int `json:"maxConcurrentDownloads"`
@ -55,7 +54,6 @@ var (
FFmpegPath: "",
AutoAddToDownloadList: false,
AutoStartAddedDownloads: false,
EnableConcurrentDownloadsLimit: false,
MaxConcurrentDownloads: 20,
@ -210,7 +208,6 @@ type RecorderSettingsPublic struct {
HasDBPassword bool `json:"hasDbPassword"`
AutoAddToDownloadList bool `json:"autoAddToDownloadList"`
AutoStartAddedDownloads bool `json:"autoStartAddedDownloads"`
EnableConcurrentDownloadsLimit bool `json:"enableConcurrentDownloadsLimit"`
MaxConcurrentDownloads int `json:"maxConcurrentDownloads"`
@ -238,7 +235,6 @@ func toPublicSettings(s RecorderSettings) RecorderSettingsPublic {
HasDBPassword: strings.TrimSpace(s.EncryptedDBPassword) != "",
AutoAddToDownloadList: s.AutoAddToDownloadList,
AutoStartAddedDownloads: s.AutoStartAddedDownloads,
EnableConcurrentDownloadsLimit: s.EnableConcurrentDownloadsLimit,
MaxConcurrentDownloads: s.MaxConcurrentDownloads,
@ -389,7 +385,6 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) {
next.EncryptedDBPassword = in.EncryptedDBPassword
next.AutoAddToDownloadList = in.AutoAddToDownloadList
next.AutoStartAddedDownloads = in.AutoStartAddedDownloads
next.EnableConcurrentDownloadsLimit = in.EnableConcurrentDownloadsLimit
next.MaxConcurrentDownloads = in.MaxConcurrentDownloads

View File

@ -589,7 +589,7 @@ export default function App() {
const [roomStatusByModelKey, setRoomStatusByModelKey] = useState<Record<string, string>>({})
const [lastHeaderUpdateAtMs, setLastHeaderUpdateAtMs] = useState<number>(() => Date.now())
const [cbOnlineFetchedAt, setCbOnlineFetchedAt] = useState<string>('')
const [modelsByKey, setModelsByKey] = useState<Record<string, StoredModel>>({})
@ -808,7 +808,6 @@ export default function App() {
const count = Number.isFinite(countRaw) && countRaw >= 0 ? countRaw : 0
setDoneCount(count)
setLastHeaderUpdateAtMs(Date.now())
} catch {
// ignore
} finally {
@ -823,7 +822,6 @@ export default function App() {
if (prefetched?.key === key && Array.isArray(prefetched.items)) {
setDoneJobs(prefetched.items)
setLastHeaderUpdateAtMs(Date.now())
donePrefetchRef.current = null
@ -852,7 +850,6 @@ export default function App() {
: []
setDoneJobs(items)
setLastHeaderUpdateAtMs(Date.now())
const countRaw = Number(data?.count ?? data?.totalCount ?? doneCount)
const count = Number.isFinite(countRaw) && countRaw >= 0 ? countRaw : 0
@ -948,7 +945,6 @@ export default function App() {
return next
})
setLastHeaderUpdateAtMs(Date.now())
} catch {
// ignore
}
@ -970,28 +966,12 @@ export default function App() {
void loadDonePage(donePage, doneSort)
}, [authed, donePage, doneSort, loadDonePage])
useEffect(() => {
setModelsCount(Object.keys(modelsByKey).length)
}, [modelsByKey])
const isChaturbateStoreModel = useCallback((m?: StoredModel | null) => {
const h = String(m?.host ?? '').toLowerCase()
const input = String(m?.input ?? '').toLowerCase()
return h.includes('chaturbate') || input.includes('chaturbate.com')
}, [])
const onlineModelsCount = useMemo(() => {
let c = 0
for (const m of Object.values(modelsByKey)) {
if (!isChaturbateStoreModel(m)) continue
if (String((m as any)?.roomStatus ?? '').trim().toLowerCase() === 'offline') continue
if ((m as any)?.isOnline === true || String((m as any)?.roomStatus ?? '').trim() !== '') {
c++
}
}
return c
}, [modelsByKey, isChaturbateStoreModel])
const applyJobDelta = useCallback((msg: JobEvent) => {
const jobId = String(msg?.jobId ?? '').trim()
if (!jobId) return
@ -1043,7 +1023,6 @@ export default function App() {
prev && String((prev as any).id ?? '').trim() === jobId ? null : prev
)
setLastHeaderUpdateAtMs(now)
return
}
@ -1080,8 +1059,6 @@ export default function App() {
if (String((prev as any).id ?? '').trim() !== jobId) return prev
return mergeJob(prev)
})
setLastHeaderUpdateAtMs(now)
}, [])
const ensureModelEventListener = useCallback((name: string) => {
@ -1194,15 +1171,75 @@ export default function App() {
})
}, [])
const refreshModelsByKey = useCallback(async () => {
try {
const list = await apiJSON<StoredModel[]>('/api/models', { cache: 'no-store' as any })
const safeList = Array.isArray(list) ? list : []
type AppModelsSnapshot = {
totalCount: number
onlineModelsCount: number
onlineWatchedModelsCount: number
onlineFavCount: number
onlineLikedCount: number
models: StoredModel[]
}
const [headerStats, setHeaderStats] = useState({
totalCount: 0,
onlineModelsCount: 0,
onlineWatchedModelsCount: 0,
onlineFavCount: 0,
onlineLikedCount: 0,
})
useEffect(() => {
setModelsCount(headerStats.totalCount)
}, [headerStats.totalCount])
const overviewInFlightRef = useRef(false)
const overviewLastSigRef = useRef('')
const overviewLastAtRef = useRef(0)
const loadAppModelsSnapshot = useCallback(async (keys: string[]) => {
const uniqKeys = Array.from(
new Set(
keys
.map((x) => String(x ?? '').trim().toLowerCase())
.filter(Boolean)
)
)
const sig = uniqKeys.join('|')
const now = Date.now()
if (overviewInFlightRef.current) return
if (overviewLastSigRef.current === sig && now - overviewLastAtRef.current < 1500) return
overviewInFlightRef.current = true
overviewLastSigRef.current = sig
overviewLastAtRef.current = now
try {
const data = await apiJSON<AppModelsSnapshot>('/api/models/overview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store' as any,
body: JSON.stringify({
keys: uniqKeys,
includeWatched: true,
}),
})
setHeaderStats({
totalCount: Number(data?.totalCount ?? 0),
onlineModelsCount: Number(data?.onlineModelsCount ?? 0),
onlineWatchedModelsCount: Number(data?.onlineWatchedModelsCount ?? 0),
onlineFavCount: Number(data?.onlineFavCount ?? 0),
onlineLikedCount: Number(data?.onlineLikedCount ?? 0),
})
const safeList = Array.isArray(data?.models) ? data.models : []
setModelsByKey(buildModelsByKey(safeList))
setLastHeaderUpdateAtMs(Date.now())
} catch {
// ignore
} finally {
overviewInFlightRef.current = false
}
}, [buildModelsByKey])
@ -1427,59 +1464,6 @@ export default function App() {
else cur.list.unshift(m)
}, [])
useEffect(() => {
// initial laden
void refreshModelsByKey()
const onChanged = (ev: Event) => {
const e = ev as CustomEvent<any>
const detail = e?.detail ?? {}
const updated = detail?.model
// ✅ 1) Update-Event mit Model: direkt in State übernehmen (KEIN /api/models)
if (updated && typeof updated === 'object') {
const k = String(updated.modelKey ?? '').toLowerCase().trim()
if (k) {
setModelsByKey((prev) => ({ ...prev, [k]: updated }))
}
try {
upsertModelCache(updated)
} catch {}
setPlayerModel((prev) => (prev?.id === updated.id ? updated : prev))
setLastHeaderUpdateAtMs(Date.now())
return
}
// ✅ 2) Delete-Event (optional): ebenfalls ohne Voll-Refresh
if (detail?.removed) {
const removedId = String(detail?.id ?? '').trim()
const removedKey = String(detail?.modelKey ?? '').toLowerCase().trim()
if (removedKey) {
setModelsByKey((prev) => {
const { [removedKey]: _drop, ...rest } = prev
return rest
})
}
if (removedId) {
setPlayerModel((prev) => (prev?.id === removedId ? null : prev))
}
setLastHeaderUpdateAtMs(Date.now())
return
}
// ✅ 3) Nur wenn kein Model/keine Delete-Info mitkommt: kompletter Refresh
void refreshModelsByKey()
}
window.addEventListener('models-changed', onChanged as any)
return () => window.removeEventListener('models-changed', onChanged as any)
}, [refreshModelsByKey, upsertModelCache])
const recSettingsRef = useRef(recSettings)
useEffect(() => {
recSettingsRef.current = recSettings
@ -1653,6 +1637,96 @@ export default function App() {
return Array.from(set)
}, [modelsByKey])
const requiredModelKeys = useMemo(() => {
const set = new Set<string>()
for (const j of jobs) {
const k = modelEventKeyFromJob(j)
if (k) set.add(k)
}
for (const j of doneJobs) {
const k = modelEventKeyFromDoneJob(j)
if (k) set.add(k)
}
for (const p of pendingWatchedRooms) {
const k = String(p?.modelKey ?? '').trim().toLowerCase()
if (k) set.add(k)
}
if (playerJob) {
const k = (modelKeyFromFilename(playerJob.output || '') || '').trim().toLowerCase()
if (k) set.add(k)
}
if (detailsModelKey) {
const k = String(detailsModelKey ?? '').trim().toLowerCase()
if (k) set.add(k)
}
return Array.from(set).sort()
}, [jobs, doneJobs, pendingWatchedRooms, playerJob, detailsModelKey])
const requiredModelKeysSig = useMemo(
() => requiredModelKeys.join('|'),
[requiredModelKeys]
)
const lastOverviewSigRef = useRef<string>('')
useEffect(() => {
if (lastOverviewSigRef.current !== requiredModelKeysSig) {
lastOverviewSigRef.current = requiredModelKeysSig
void loadAppModelsSnapshot(requiredModelKeys)
}
const onChanged = (ev: Event) => {
const e = ev as CustomEvent<any>
const detail = e?.detail ?? {}
const updated = detail?.model
if (updated && typeof updated === 'object') {
const k = String(updated.modelKey ?? '').toLowerCase().trim()
if (k) {
setModelsByKey((prev) => ({ ...prev, [k]: updated }))
}
try {
upsertModelCache(updated)
} catch {}
setPlayerModel((prev) => (prev?.id === updated.id ? updated : prev))
return
}
if (detail?.removed) {
const removedId = String(detail?.id ?? '').trim()
const removedKey = String(detail?.modelKey ?? '').toLowerCase().trim()
if (removedKey) {
setModelsByKey((prev) => {
const { [removedKey]: _drop, ...rest } = prev
return rest
})
}
if (removedId) {
setPlayerModel((prev) => (prev?.id === removedId ? null : prev))
}
return
}
void loadAppModelsSnapshot(requiredModelKeys)
}
window.addEventListener('models-changed', onChanged as any)
return () => window.removeEventListener('models-changed', onChanged as any)
}, [loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
const selectedTabRef = useRef(selectedTab)
const applyPendingRoomSnapshot = useCallback(
@ -1752,7 +1826,10 @@ export default function App() {
}, [pendingAutoStartByKey, modelsByKey])
useEffect(() => {
if (!recSettings.useChaturbateApi) return
if (!recSettings.useChaturbateApi) {
setCbOnlineFetchedAt('')
return
}
let cancelled = false
let timer: number | null = null
@ -1789,6 +1866,7 @@ export default function App() {
if (!res.ok) return
const data = (await res.json().catch(() => null)) as ChaturbateOnlineResponse | null
const rooms = Array.isArray(data?.rooms) ? data!.rooms : []
const byKey: Record<
@ -1892,6 +1970,7 @@ export default function App() {
}
const data = (await res.json().catch(() => null)) as ChaturbateOnlineResponse | null
const room = Array.isArray(data?.rooms)
? data!.rooms.find((x) => String(x?.username ?? '').trim().toLowerCase() === keyLower)
: null
@ -1949,6 +2028,48 @@ export default function App() {
setRoomStatusByModelKey(next)
}, [jobs, modelsByKey, recSettings.useChaturbateApi])
useEffect(() => {
if (!authed || !recSettings.useChaturbateApi) {
setCbOnlineFetchedAt('')
return
}
let cancelled = false
let timer: number | null = null
const tick = async () => {
try {
const onlineRes = await fetch('/api/chaturbate/online', {
method: 'GET',
cache: 'no-store',
})
if (!onlineRes.ok) return
const data = (await onlineRes.json().catch(() => null)) as ChaturbateOnlineResponse | null
if (cancelled || !data) return
const fetchedAt = String(data.fetchedAt ?? '').trim()
if (fetchedAt && fetchedAt !== '0001-01-01T00:00:00Z') {
setCbOnlineFetchedAt(fetchedAt)
}
} catch {
// ignore
}
}
void tick()
timer = window.setInterval(() => {
void tick()
}, 10_000)
return () => {
cancelled = true
if (timer != null) window.clearInterval(timer)
}
}, [authed, recSettings.useChaturbateApi])
function shouldQueueForRoomStatus(
show: string
): boolean {
@ -2234,37 +2355,10 @@ export default function App() {
}, [])
// ✅ Anzahl Watched Models (aus Store), die online sind
const onlineWatchedModelsCount = useMemo(() => {
let c = 0
for (const m of Object.values(modelsByKey)) {
if (!m?.watching) continue
if (!isChaturbateStoreModel(m)) continue
const k = lower(String(m?.modelKey ?? ''))
if (!k) continue
const status = String((m as any)?.roomStatus ?? '').trim().toLowerCase()
if (status && status !== 'offline') c++
}
return c
}, [modelsByKey, isChaturbateStoreModel])
const { onlineFavCount, onlineLikedCount } = useMemo(() => {
let fav = 0
let liked = 0
for (const m of Object.values(modelsByKey)) {
const k = lower(String(m?.modelKey ?? ''))
if (!k) continue
const status = String((m as any)?.roomStatus ?? '').trim().toLowerCase()
if (!status || status === 'offline') continue
if (m?.favorite) fav++
if (m?.liked === true) liked++
}
return { onlineFavCount: fav, onlineLikedCount: liked }
}, [modelsByKey])
const onlineModelsCount = headerStats.onlineModelsCount
const onlineWatchedModelsCount = headerStats.onlineWatchedModelsCount
const onlineFavCount = headerStats.onlineFavCount
const onlineLikedCount = headerStats.onlineLikedCount
const runningTabCount = useMemo(() => {
const activeDownloads = jobs.filter((j) => {
@ -3413,14 +3507,20 @@ export default function App() {
</span>
</div>
<div className="hidden sm:block text-[11px] text-gray-500 dark:text-gray-400">
<LastUpdatedText lastAt={lastHeaderUpdateAtMs} />
<LastUpdatedText
enabled={Boolean(recSettings.useChaturbateApi)}
fetchedAt={cbOnlineFetchedAt}
/>
</div>
</div>
{/* ✅ Mobile: Status volle Breite + PerfMonitor + Cookies nebeneinander */}
<div className="sm:hidden mt-1 w-full">
<div className="text-[11px] text-gray-500 dark:text-gray-400">
<LastUpdatedText lastAt={lastHeaderUpdateAtMs} />
<LastUpdatedText
enabled={Boolean(recSettings.useChaturbateApi)}
fetchedAt={cbOnlineFetchedAt}
/>
</div>
<div className="mt-2 flex items-stretch gap-2">

View File

@ -1,8 +1,10 @@
// frontend\src\components\ui\LastUpdatedText.tsx
import { useEffect, useMemo, useState } from 'react'
function formatAgoDE(diffMs: number) {
const s = Math.max(0, Math.floor(diffMs / 1000))
if (s < 2) return 'gerade eben'
if (s < 5) return 'gerade eben'
if (s < 60) return `vor ${s} Sekunden`
const m = Math.floor(s / 60)
@ -11,20 +13,41 @@ function formatAgoDE(diffMs: number) {
const h = Math.floor(m / 60)
if (h === 1) return 'vor 1 Stunde'
return `vor ${h} Stunden`
if (h < 24) return `vor ${h} Stunden`
const d = Math.floor(h / 24)
if (d === 1) return 'vor 1 Tag'
return `vor ${d} Tagen`
}
export default function LastUpdatedText({ lastAt }: { lastAt: number }) {
type Props = {
fetchedAt?: string | null
enabled?: boolean
}
export default function LastUpdatedText({ fetchedAt, enabled = true }: Props) {
const [now, setNow] = useState(() => Date.now())
useEffect(() => {
const t = window.setInterval(() => setNow(Date.now()), 1000)
if (!enabled) return
const t = window.setInterval(() => setNow(Date.now()), 10_000)
return () => window.clearInterval(t)
}, [])
}, [enabled])
const text = useMemo(() => {
return `(zuletzt aktualisiert: ${formatAgoDE(now - lastAt)})`
}, [now, lastAt])
if (!enabled) return ''
const raw = String(fetchedAt ?? '').trim()
if (!raw) return '(zuletzt aktualisiert: noch kein erfolgreicher Pull)'
const ts = Date.parse(raw)
if (!Number.isFinite(ts)) {
return '(zuletzt aktualisiert: unbekannt)'
}
return `(zuletzt aktualisiert: ${formatAgoDE(now - ts)})`
}, [enabled, fetchedAt, now])
return <>{text}</>
}

View File

@ -18,7 +18,6 @@ type RecorderSettings = {
doneDir: string
ffmpegPath?: string
autoAddToDownloadList?: boolean
autoStartAddedDownloads?: boolean
enableConcurrentDownloadsLimit?: boolean
maxConcurrentDownloads?: number
useChaturbateApi?: boolean
@ -188,7 +187,6 @@ 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:
@ -522,7 +520,6 @@ 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,
@ -557,7 +554,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
doneDir,
ffmpegPath,
autoAddToDownloadList,
autoStartAddedDownloads,
enableConcurrentDownloadsLimit,
maxConcurrentDownloads,
useChaturbateApi,
@ -1111,21 +1107,12 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
setValue((v) => ({
...v,
autoAddToDownloadList: checked,
autoStartAddedDownloads: checked ? v.autoStartAddedDownloads : false,
}))
}
label="Automatisch zur Downloadliste hinzufügen"
description="Neue Links/Modelle werden automatisch in die Downloadliste übernommen."
/>
<LabeledSwitch
checked={!!value.autoStartAddedDownloads}
onChange={(checked) => setValue((v) => ({ ...v, autoStartAddedDownloads: checked }))}
disabled={!value.autoAddToDownloadList}
label="Hinzugefügte Downloads automatisch starten"
description="Wenn ein Download hinzugefügt wurde, startet er direkt (sofern möglich)."
/>
<LabeledSwitch
checked={!!value.useChaturbateApi}
onChange={(checked) => setValue((v) => ({ ...v, useChaturbateApi: checked }))}