510 lines
10 KiB
Go
510 lines
10 KiB
Go
// 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)
|
||
}
|
||
}
|
||
}
|
||
}
|