nsfwapp/backend/myfreecams_autostart.go
2026-01-13 14:00:05 +01:00

186 lines
4.1 KiB
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 = 12 * 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 {
notifyJobsChanged()
}
}