549 lines
12 KiB
Go
549 lines
12 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
|
||
|
||
const snapshotMaxAge = 90 * 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
|
||
|
||
rooms, fetchedAt, fresh := getChaturbateRoomsSnapshot(snapshotMaxAge)
|
||
if !fresh {
|
||
if verboseLogs() {
|
||
if fetchedAt.IsZero() {
|
||
fmt.Println("⚠️ [autostart] chaturbate snapshot missing")
|
||
} else {
|
||
fmt.Println("⚠️ [autostart] chaturbate snapshot stale:", time.Since(fetchedAt).Round(time.Second))
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
showByUser := map[string]string{}
|
||
imageByUser := map[string]string{}
|
||
seenInAPI := map[string]bool{}
|
||
|
||
for _, r := range rooms {
|
||
u := normUser(r.Username)
|
||
if u == "" {
|
||
continue
|
||
}
|
||
seenInAPI[u] = true
|
||
showByUser[u] = normalizePendingShowServer(r.CurrentShow)
|
||
imageByUser[u] = selectBestRoomImageURL(r)
|
||
}
|
||
|
||
// laufende Jobs sammeln
|
||
runningByUser := map[string]*RecordJob{}
|
||
jobsMu.RLock()
|
||
for _, j := range jobs {
|
||
if j == nil {
|
||
continue
|
||
}
|
||
if !isActiveRecordingJob(j) {
|
||
continue
|
||
}
|
||
if detectProvider(strings.TrimSpace(j.SourceURL)) != "chaturbate" {
|
||
continue
|
||
}
|
||
u := chaturbateUserFromURL(j.SourceURL)
|
||
if u != "" {
|
||
runningByUser[u] = j
|
||
}
|
||
}
|
||
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 runningByUser[it.userKey] != nil {
|
||
continue
|
||
}
|
||
|
||
it.url = resolveChaturbateURL(m)
|
||
if it.url == "" {
|
||
continue
|
||
}
|
||
|
||
nextQueue = append(nextQueue, it)
|
||
nextQueued[it.userKey] = true
|
||
}
|
||
queue = nextQueue
|
||
queued = nextQueued
|
||
|
||
now := time.Now()
|
||
nextPending := make([]PendingAutoStartItem, 0, len(watchedByUser))
|
||
|
||
for user, m := range watchedByUser {
|
||
u := resolveChaturbateURL(m)
|
||
if u == "" {
|
||
continue
|
||
}
|
||
|
||
show := normalizePendingShowServer(showByUser[user])
|
||
img := strings.TrimSpace(imageByUser[user])
|
||
|
||
switch show {
|
||
case "public":
|
||
// public => kein Pending-Eintrag
|
||
if runningByUser[user] != nil {
|
||
continue
|
||
}
|
||
if queued[user] {
|
||
continue
|
||
}
|
||
if t, ok := lastTry[user]; ok && now.Sub(t) < retryCooldown {
|
||
continue
|
||
}
|
||
|
||
queue = append(queue, autoStartItem{
|
||
userKey: user,
|
||
url: u,
|
||
})
|
||
queued[user] = true
|
||
|
||
case "private", "hidden", "away":
|
||
// laufenden watched-Download beenden und auf public warten
|
||
nextPending = append(nextPending, PendingAutoStartItem{
|
||
ModelKey: user,
|
||
URL: u,
|
||
Mode: "wait_public",
|
||
CurrentShow: show,
|
||
ImageURL: img,
|
||
Source: "watched",
|
||
})
|
||
|
||
if runningJob := runningByUser[user]; runningJob != nil {
|
||
stopJobsInternal([]*RecordJob{runningJob})
|
||
}
|
||
|
||
default:
|
||
// offline / unknown / nicht in API
|
||
nextProbeAtMs := now.Add(blindRetryCooldown).UnixMilli()
|
||
|
||
nextPending = append(nextPending, PendingAutoStartItem{
|
||
ModelKey: user,
|
||
URL: u,
|
||
Mode: "probe_retry",
|
||
NextProbeAtMs: nextProbeAtMs,
|
||
CurrentShow: show,
|
||
ImageURL: img,
|
||
Source: "watched",
|
||
})
|
||
|
||
if runningByUser[user] != nil {
|
||
continue
|
||
}
|
||
if queued[user] {
|
||
continue
|
||
}
|
||
if t, ok := lastBlindTry[user]; ok && now.Sub(t) < blindRetryCooldown {
|
||
continue
|
||
}
|
||
|
||
queue = append(queue, autoStartItem{
|
||
userKey: user,
|
||
url: u,
|
||
})
|
||
queued[user] = true
|
||
}
|
||
}
|
||
|
||
pendingAutoStartMu.Lock()
|
||
_ = saveWatchedPendingAutoStartItems(nextPending)
|
||
pendingAutoStartMu.Unlock()
|
||
|
||
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
|
||
}
|
||
|
||
rooms, _, fresh := getChaturbateRoomsSnapshot(snapshotMaxAge)
|
||
if !fresh {
|
||
continue
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
pendingAutoStartMu.Lock()
|
||
_ = removeWatchedPendingAutoStartItem(it.userKey)
|
||
pendingAutoStartMu.Unlock()
|
||
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
}
|