// backend\chaturbate_autostart.go package main import ( "fmt" "net/url" "os" "sort" "strings" "time" ) type autoStartItem struct { userKey string url string } func normUser(s string) string { return strings.ToLower(strings.TrimSpace(s)) } func chaturbateUserFromURL(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } u, err := url.Parse(raw) if err != nil || u.Hostname() == "" { return "" } host := strings.ToLower(u.Hostname()) if !strings.Contains(host, "chaturbate.com") { return "" } parts := strings.Split(u.Path, "/") for _, p := range parts { p = strings.TrimSpace(p) if p != "" { return normUser(p) } } return "" } func cookieHeaderFromSettings(s RecorderSettings) string { m, err := decryptCookieMap(s.EncryptedCookies) if err != nil || len(m) == 0 { return "" } keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) var b strings.Builder first := true for _, k := range keys { v := strings.TrimSpace(m[k]) k = strings.TrimSpace(k) if k == "" || v == "" { continue } if !first { b.WriteString("; ") } first = false b.WriteString(k) b.WriteString("=") b.WriteString(v) } return b.String() } func resolveChaturbateURL(m WatchedModelLite) string { in := strings.TrimSpace(m.Input) if strings.HasPrefix(strings.ToLower(in), "http://") || strings.HasPrefix(strings.ToLower(in), "https://") { return in } key := strings.Trim(strings.TrimSpace(m.ModelKey), "/") if key == "" { return "" } return fmt.Sprintf("https://chaturbate.com/%s/", key) } func chaturbateAbortIfNoOutput(jobID string, maxWait time.Duration) { deadline := time.Now().Add(maxWait) for time.Now().Before(deadline) { jobsMu.RLock() job := jobs[jobID] status := JobStatus("") out := "" hidden := false if job != nil { status = job.Status out = strings.TrimSpace(job.Output) hidden = job.Hidden } jobsMu.RUnlock() // Job schon weg oder nicht mehr running -> nichts tun if job == nil || status != JobRunning { return } // echte Output-Datei vorhanden? if out != "" { if fi, err := os.Stat(out); err == nil && !fi.IsDir() && fi.Size() > 0 { // hidden blind-try wird jetzt sichtbar gemacht if hidden { jobsMu.Lock() j := jobs[jobID] if j != nil { j.Hidden = false } jobsMu.Unlock() } publishJob(jobID) return } } time.Sleep(900 * time.Millisecond) } // nach Wartezeit immer noch keine Datei => stoppen + löschen jobsMu.Lock() job := jobs[jobID] if job == nil || job.Status != JobRunning { jobsMu.Unlock() return } pc := job.previewCmd job.previewCmd = nil previewCancel := job.previewCancel job.previewCancel = nil recordCancel := job.cancel out := strings.TrimSpace(job.Output) wasVisible := !job.Hidden jobsMu.Unlock() if previewCancel != nil { previewCancel() } if pc != nil && pc.Process != nil { _ = pc.Process.Kill() } if recordCancel != nil { recordCancel() } // 0-Byte Datei ggf. löschen if out != "" { if fi, err := os.Stat(out); err == nil && !fi.IsDir() && fi.Size() == 0 { _ = os.Remove(out) } } jobsMu.Lock() j := jobs[jobID] delete(jobs, jobID) jobsMu.Unlock() if wasVisible && j != nil { publishJobRemove(j) } } // Startet watched+online(public) automatisch – unabhängig vom Frontend func startChaturbateAutoStartWorker(store *ModelStore) { if store == nil { if verboseLogs() { fmt.Println("⚠️ [autostart] model store is nil") } return } const pollInterval = 5 * time.Second const startGap = 1500 * time.Millisecond // normaler Retry für API-public const retryCooldown = 25 * time.Second // aggressiver vermeiden für "nicht in API, aber watched" Blind-Try const blindRetryCooldown = 2 * time.Minute // wie lange wir warten, ob eine echte Datei entsteht const outputProbeMax = 20 * time.Second queue := make([]autoStartItem, 0, 64) queued := map[string]bool{} lastTry := map[string]time.Time{} lastBlindTry := map[string]time.Time{} var lastStart time.Time for { if isAutostartPaused() { time.Sleep(2 * time.Second) continue } s := getSettings() if !s.UseChaturbateAPI { queue = queue[:0] queued = map[string]bool{} time.Sleep(2 * time.Second) continue } cookieHdr := cookieHeaderFromSettings(s) if !hasChaturbateCookies(cookieHdr) { time.Sleep(5 * time.Second) continue } // online snapshot aus cache cbMu.RLock() rooms := append([]ChaturbateRoom(nil), cb.Rooms...) cbMu.RUnlock() showByUser := map[string]string{} seenInAPI := map[string]bool{} for _, r := range rooms { u := normUser(r.Username) if u == "" { continue } seenInAPI[u] = true showByUser[u] = strings.ToLower(strings.TrimSpace(r.CurrentShow)) } // laufende Jobs sammeln running := map[string]bool{} jobsMu.RLock() for _, j := range jobs { if !isActiveRecordingJob(j) { continue } u := chaturbateUserFromURL(j.SourceURL) if u != "" { running[u] = true } } jobsMu.RUnlock() // watched list aus DB watched := store.ListWatchedLite("chaturbate.com") watchedByUser := map[string]WatchedModelLite{} for _, m := range watched { key := normUser(m.ModelKey) if key != "" && m.Watching { watchedByUser[key] = m } } // queue prune nextQueue := queue[:0] nextQueued := map[string]bool{} for _, it := range queue { m, ok := watchedByUser[it.userKey] if !ok { continue } if running[it.userKey] { continue } it.url = resolveChaturbateURL(m) if it.url == "" { continue } nextQueue = append(nextQueue, it) nextQueued[it.userKey] = true } queue = nextQueue queued = nextQueued now := time.Now() // ------------------------------------------------------------ // 1) watched + API sagt public => normal enqueue // ------------------------------------------------------------ for user, m := range watchedByUser { if showByUser[user] != "public" { continue } if running[user] { continue } if queued[user] { continue } if t, ok := lastTry[user]; ok && now.Sub(t) < retryCooldown { continue } u := resolveChaturbateURL(m) if u == "" { continue } queue = append(queue, autoStartItem{ userKey: user, url: u, }) queued[user] = true } // ------------------------------------------------------------ // 2) watched + NICHT in API => blind best-effort try wie MFC // aber nur wenn: // - nicht schon running // - nicht schon queued // - nicht kürzlich versucht // ------------------------------------------------------------ for user, m := range watchedByUser { if seenInAPI[user] { continue } if running[user] { continue } if queued[user] { continue } if t, ok := lastBlindTry[user]; ok && now.Sub(t) < blindRetryCooldown { continue } u := resolveChaturbateURL(m) if u == "" { continue } // direkt "blind" starten, nicht erst in queue hängen, // damit das Verhalten wie MFC ist if !lastStart.IsZero() && time.Since(lastStart) < startGap { continue } lastBlindTry[user] = time.Now() job, err := startRecordingInternal(RecordRequest{ URL: u, Cookie: cookieHdr, Hidden: true, }) if err != nil || job == nil { if verboseLogs() { fmt.Println("❌ [autostart] blind start failed:", u, err) } continue } if verboseLogs() { fmt.Println("▶️ [autostart] blind try started:", u) } go chaturbateAbortIfNoOutput(job.ID, outputProbeMax) lastStart = time.Now() } // ------------------------------------------------------------ // 3) normale API-public queue abarbeiten // ------------------------------------------------------------ if len(queue) > 0 && (lastStart.IsZero() || time.Since(lastStart) >= startGap) { it := queue[0] queue = queue[1:] delete(queued, it.userKey) lastTry[it.userKey] = time.Now() job, err := startRecordingInternal(RecordRequest{ URL: it.url, Cookie: cookieHdr, }) if err != nil { if verboseLogs() { fmt.Println("❌ [autostart] start failed:", it.url, err) } } else { if verboseLogs() { fmt.Println("▶️ [autostart] started:", it.url) } // optional auch hier absichern; schadet nicht if job != nil { go chaturbateAbortIfNoOutput(job.ID, outputProbeMax) } lastStart = time.Now() } } time.Sleep(pollInterval) } }