diff --git a/backend/chaturbate_autostart.go b/backend/chaturbate_autostart.go index 984311e..49ec31b 100644 --- a/backend/chaturbate_autostart.go +++ b/backend/chaturbate_autostart.go @@ -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 } diff --git a/backend/models_api.go b/backend/models_api.go index 52593b4..8824341 100644 --- a/backend/models_api.go +++ b/backend/models_api.go @@ -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"}) diff --git a/backend/recorder_settings.json b/backend/recorder_settings.json index b8f0572..cb6f73b 100644 --- a/backend/recorder_settings.json +++ b/backend/recorder_settings.json @@ -5,7 +5,6 @@ "doneDir": "C:\\Users\\Rother\\Desktop\\Privat\\records\\done", "ffmpegPath": "", "autoAddToDownloadList": true, - "autoStartAddedDownloads": true, "enableConcurrentDownloadsLimit": true, "maxConcurrentDownloads": 80, "useChaturbateApi": true, diff --git a/backend/settings.go b/backend/settings.go index 17e26fb..4c909dc 100644 --- a/backend/settings.go +++ b/backend/settings.go @@ -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 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eebc480..4942875 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -589,7 +589,7 @@ export default function App() { const [roomStatusByModelKey, setRoomStatusByModelKey] = useState>({}) - const [lastHeaderUpdateAtMs, setLastHeaderUpdateAtMs] = useState(() => Date.now()) + const [cbOnlineFetchedAt, setCbOnlineFetchedAt] = useState('') const [modelsByKey, setModelsByKey] = useState>({}) @@ -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,27 +966,11 @@ 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() @@ -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('/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('/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 - 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() + + 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('') + + useEffect(() => { + if (lastOverviewSigRef.current !== requiredModelKeysSig) { + lastOverviewSigRef.current = requiredModelKeysSig + void loadAppModelsSnapshot(requiredModelKeys) + } + + const onChanged = (ev: Event) => { + const e = ev as CustomEvent + 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() {
- +
{/* ✅ Mobile: Status volle Breite + PerfMonitor + Cookies nebeneinander */}
- +
diff --git a/frontend/src/components/ui/LastUpdatedText.tsx b/frontend/src/components/ui/LastUpdatedText.tsx index caa2f86..1c101fd 100644 --- a/frontend/src/components/ui/LastUpdatedText.tsx +++ b/frontend/src/components/ui/LastUpdatedText.tsx @@ -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} } \ No newline at end of file diff --git a/frontend/src/components/ui/RecorderSettings.tsx b/frontend/src/components/ui/RecorderSettings.tsx index 79f834c..fa67ab9 100644 --- a/frontend/src/components/ui/RecorderSettings.tsx +++ b/frontend/src/components/ui/RecorderSettings.tsx @@ -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." /> - 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)." - /> - setValue((v) => ({ ...v, useChaturbateApi: checked }))}