// backend\myfreecams_autostart.go package main import ( "os" "strings" "time" ) // Startet watched MyFreeCams Models (ohne API) "best-effort". // Wenn nach kurzer Zeit keine Output-Datei existiert (oder 0 Bytes), wird abgebrochen und der Job wieder entfernt. func startMyFreeCamsAutoStartWorker(store *ModelStore) { if store == nil { return } // pro Model: Retry-Cooldown, damit du nicht permanent die gleichen Models spamst const cooldown = 2 * time.Minute // wie lange wir nach Start warten, ob eine Datei entsteht const outputProbeMax = 20 * time.Second lastAttempt := map[string]time.Time{} tick := time.NewTicker(6 * time.Second) defer tick.Stop() for range tick.C { s := getSettings() if !s.UseMyFreeCamsWatcher { continue } // watched Models aus DB watched := store.ListWatchedLite("myfreecams.com") if len(watched) == 0 { continue } // langsam nacheinander starten (keine API -> einzelnes "Anprobieren") for _, m := range watched { // ✅ Wenn User den Switch während eines Ticks deaktiviert, sofort stoppen if !getSettings().UseMyFreeCamsWatcher { break } // ✅ Wenn im UI "Alle Stoppen" -> Autostart pausiert, sofort aufhören if isAutostartPaused() { break } u := strings.TrimSpace(m.Input) if u == "" { continue } modelID := strings.TrimSpace(m.ID) if modelID == "" { // Fallback modelID = strings.TrimSpace(m.Host) + ":" + strings.TrimSpace(m.ModelKey) } // Cooldown if t, ok := lastAttempt[modelID]; ok && time.Since(t) < cooldown { continue } // bereits als Job aktiv? if isJobRunningForURL(u) { continue } lastAttempt[modelID] = time.Now() job, err := startRecordingInternal(RecordRequest{URL: u, Hidden: true}) if err != nil || job == nil { continue } // Output prüfen: wenn nichts entsteht -> abbrechen + aus jobs entfernen go mfcAbortIfNoOutput(job.ID, outputProbeMax) // kleine Pause, damit du nicht 20 Models in einem Tick startest time.Sleep(1200 * time.Millisecond) } } } func isJobRunningForURL(u string) bool { u = strings.TrimSpace(u) if u == "" { return false } jobsMu.Lock() defer jobsMu.Unlock() for _, j := range jobs { if j == nil { continue } if j.Status == JobRunning && strings.TrimSpace(j.SourceURL) == u { return true } } return false } // Wenn nach maxWait keine Output-Datei (>0 Bytes) existiert, stoppen + Job entfernen. // Hintergrund: bei MFC kann "offline/away/private" sein => keine Ausgabe entsteht. func mfcAbortIfNoOutput(jobID string, maxWait time.Duration) { deadline := time.Now().Add(maxWait) for time.Now().Before(deadline) { jobsMu.Lock() job := jobs[jobID] status := JobStatus("") out := "" if job != nil { status = job.Status out = strings.TrimSpace(job.Output) } jobsMu.Unlock() // Job schon weg oder nicht mehr running -> nix tun if job == nil || status != JobRunning { return } // Output schon da? if out != "" { if fi, err := os.Stat(out); err == nil && !fi.IsDir() && fi.Size() > 0 { // ✅ jetzt ist es ein "echter" Download -> im UI sichtbar machen 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 } // Snapshot: was wir ohne Lock beenden können pc := job.previewCmd job.previewCmd = nil cancel := job.cancel out := strings.TrimSpace(job.Output) jobsMu.Unlock() // preview kill if pc != nil && pc.Process != nil { _ = pc.Process.Kill() } // recording cancel if cancel != nil { cancel() } // 0-Byte Datei ggf. wegwerfen (damit "leere Starts" nicht im recordDir liegen bleiben) if out != "" { if fi, err := os.Stat(out); err == nil && !fi.IsDir() && fi.Size() == 0 { _ = os.Remove(out) } } // Job aus der Liste entfernen (UI bleibt sauber) jobsMu.Lock() j := jobs[jobID] wasVisible := (j != nil && !j.Hidden) delete(jobs, jobID) jobsMu.Unlock() // ✅ wenn der Job nie sichtbar war, nicht unnötig UI refreshen if wasVisible { notifyJobRemoved(jobID) } }