nsfwapp/backend/chaturbate_autostart.go
2026-04-07 07:20:08 +02:00

414 lines
8.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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)
}
}