nsfwapp/backend/chaturbate_autostart.go
2026-04-07 11:46:42 +02:00

510 lines
10 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 watchedChaturbateUserKey(m WatchedModelLite) string {
key := normUser(m.ModelKey)
if key != "" {
return key
}
in := strings.TrimSpace(m.Input)
if in == "" {
return ""
}
// zuerst echte URL parsen
if u := chaturbateUserFromURL(in); u != "" {
return u
}
// fallback: roher input könnte schon nur der username sein
return normUser(strings.Trim(in, "/"))
}
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 == "" {
key = watchedChaturbateUserKey(m)
}
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 scanInterval = 1 * time.Second
const startInterval = 5 * time.Second
// 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{}
scanTicker := time.NewTicker(scanInterval)
startTicker := time.NewTicker(startInterval)
defer scanTicker.Stop()
defer startTicker.Stop()
var lastCookieHdr string
for {
select {
case <-scanTicker.C:
if isAutostartPaused() {
continue
}
s := getSettings()
if !s.UseChaturbateAPI {
queue = queue[:0]
queued = map[string]bool{}
lastCookieHdr = ""
continue
}
cookieHdr := cookieHeaderFromSettings(s)
if !hasChaturbateCookies(cookieHdr) {
lastCookieHdr = ""
continue
}
lastCookieHdr = cookieHdr
// 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
// bewusst OHNE host-filter laden, weil Altbestände evtl. keinen sauberen host haben
watched := store.ListWatchedLite("")
watchedByUser := map[string]WatchedModelLite{}
for _, m := range watched {
if !m.Watching {
continue
}
// nur Chaturbate-Einträge berücksichtigen
host := strings.ToLower(strings.TrimSpace(m.Host))
host = strings.TrimPrefix(host, "www.")
if host != "" && host != "chaturbate.com" {
// Falls Host gesetzt und nicht chaturbate -> ignorieren
continue
}
// Falls Host leer ist, versuchen wir über Input/ModelKey zu erkennen
key := watchedChaturbateUserKey(m)
if key == "" {
continue
}
// Wenn Host leer ist, aber Input eindeutig nicht Chaturbate ist -> skip
if host == "" {
in := strings.TrimSpace(m.Input)
if in != "" {
lowerIn := strings.ToLower(in)
if strings.Contains(lowerIn, "http://") || strings.Contains(lowerIn, "https://") {
if chaturbateUserFromURL(in) == "" {
continue
}
}
}
}
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 {
show := strings.ToLower(strings.TrimSpace(showByUser[user]))
if show == "" {
continue
}
if !strings.Contains(show, "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 vormerken
// ------------------------------------------------------------
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
}
queue = append(queue, autoStartItem{
userKey: user,
url: u,
})
queued[user] = true
}
case <-startTicker.C:
if isAutostartPaused() {
continue
}
s := getSettings()
if !s.UseChaturbateAPI {
continue
}
if !hasChaturbateCookies(lastCookieHdr) {
continue
}
if len(queue) == 0 {
continue
}
it := queue[0]
queue = queue[1:]
delete(queued, it.userKey)
// prüfen, ob inzwischen schon aktiv
if isJobRunningForURL(it.url) {
continue
}
// unterscheiden: API-public oder Blind-Try
cbMu.RLock()
rooms := append([]ChaturbateRoom(nil), cb.Rooms...)
cbMu.RUnlock()
showByUser := map[string]string{}
for _, r := range rooms {
u := normUser(r.Username)
if u == "" {
continue
}
showByUser[u] = strings.ToLower(strings.TrimSpace(r.CurrentShow))
}
show := strings.TrimSpace(showByUser[it.userKey])
isPublic := strings.Contains(show, "public")
if isPublic {
lastTry[it.userKey] = time.Now()
job, err := startRecordingInternal(RecordRequest{
URL: it.url,
Cookie: lastCookieHdr,
})
if err != nil {
if verboseLogs() {
fmt.Println("❌ [autostart] start failed:", it.url, err)
}
continue
}
if verboseLogs() {
fmt.Println("▶️ [autostart] started:", it.url)
}
if job != nil {
go chaturbateAbortIfNoOutput(job.ID, outputProbeMax)
}
} else {
lastBlindTry[it.userKey] = time.Now()
job, err := startRecordingInternal(RecordRequest{
URL: it.url,
Cookie: lastCookieHdr,
Hidden: true,
})
if err != nil || job == nil {
if verboseLogs() {
fmt.Println("❌ [autostart] blind start failed:", it.url, err)
}
continue
}
if verboseLogs() {
fmt.Println("▶️ [autostart] blind try started:", it.url)
}
go chaturbateAbortIfNoOutput(job.ID, outputProbeMax)
}
}
}
}