// 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 nicht ständig dieselben Models angestoßen werden const cooldown = 2 * time.Minute // wie lange wir nach Start warten, ob eine Output-Datei entsteht const outputProbeMax = 20 * time.Second // kleine Pause zwischen Starts, damit nicht zu viele Jobs auf einmal anlaufen const startGap = 1200 * time.Millisecond lastAttempt := map[string]time.Time{} tick := time.NewTicker(6 * time.Second) defer tick.Stop() for range tick.C { // global früh abbrechen if isAutostartPaused() { continue } 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 { // während des Loops erneut prüfen, falls User live stoppt if isAutostartPaused() { break } if !getSettings().UseMyFreeCamsWatcher { 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) } if modelID == "" { continue } // Cooldown if t, ok := lastAttempt[modelID]; ok && time.Since(t) < cooldown { continue } // Bereits aktiv? if isJobRunningForURL(u) { continue } lastAttempt[modelID] = time.Now() job, err := startRecordingInternal(RecordRequest{ URL: u, Hidden: true, }) if err != nil || job == nil { continue } // Wenn keine echte Output-Datei entsteht -> Job wieder abbrechen/aufräumen go mfcAbortIfNoOutput(job.ID, outputProbeMax) time.Sleep(startGap) } } } func isJobRunningForURL(u string) bool { u = strings.TrimSpace(u) if u == "" { return false } jobsMu.RLock() defer jobsMu.RUnlock() 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.RLock() job := jobs[jobID] status := JobStatus("") out := "" if job != nil { status = job.Status out = strings.TrimSpace(job.Output) } jobsMu.RUnlock() // Job schon weg oder nicht mehr running -> nichts 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 { // 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 } pc := job.previewCmd job.previewCmd = nil previewCancel := job.previewCancel job.previewCancel = nil recordCancel := job.cancel out := strings.TrimSpace(job.Output) jobsMu.Unlock() if previewCancel != nil { previewCancel() } if pc != nil && pc.Process != nil { _ = pc.Process.Kill() } if recordCancel != nil { recordCancel() } // 0-Byte Datei ggf. wegwerfen if out != "" { if fi, err := os.Stat(out); err == nil && !fi.IsDir() && fi.Size() == 0 { _ = os.Remove(out) } } jobsMu.Lock() j := jobs[jobID] wasVisible := (j != nil && !j.Hidden) delete(jobs, jobID) jobsMu.Unlock() if wasVisible { publishJobRemove(j) } }