1041 lines
22 KiB
Go
1041 lines
22 KiB
Go
// backend\chaturbate_autostart.go
|
||
|
||
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
type autoStartItem struct {
|
||
userKey string
|
||
url string
|
||
blind bool
|
||
source string // "watched" | "manual" | "resume"
|
||
force bool // true = ignoriert Autostart-Pause + Download-Limit
|
||
}
|
||
|
||
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 ""
|
||
}
|
||
|
||
lower := strings.ToLower(raw)
|
||
if !strings.Contains(lower, "://") {
|
||
if strings.HasPrefix(lower, "chaturbate.com/") || strings.HasPrefix(lower, "www.chaturbate.com/") {
|
||
raw = "https://" + raw
|
||
} else {
|
||
return ""
|
||
}
|
||
}
|
||
|
||
u, err := url.Parse(raw)
|
||
if err != nil || u.Hostname() == "" {
|
||
return ""
|
||
}
|
||
|
||
host := strings.ToLower(strings.TrimSpace(u.Hostname()))
|
||
host = strings.TrimPrefix(host, "www.")
|
||
if host != "chaturbate.com" && !strings.HasSuffix(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 {
|
||
if u := chaturbateUserFromURL(m.Input); u != "" {
|
||
return fmt.Sprintf("https://chaturbate.com/%s/", u)
|
||
}
|
||
|
||
key := watchedChaturbateUserKey(m)
|
||
if key == "" {
|
||
return ""
|
||
}
|
||
|
||
return fmt.Sprintf("https://chaturbate.com/%s/", key)
|
||
}
|
||
|
||
func chaturbateAbortIfNoOutput(jobID string, maxWait time.Duration, onOutput func(), onNoOutput func()) {
|
||
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()
|
||
}
|
||
|
||
if onOutput != nil {
|
||
onOutput()
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
if onNoOutput != nil {
|
||
onNoOutput()
|
||
}
|
||
|
||
jobsMu.Lock()
|
||
j := jobs[jobID]
|
||
delete(jobs, jobID)
|
||
jobsMu.Unlock()
|
||
|
||
if wasVisible && j != nil {
|
||
publishJobRemove(j)
|
||
}
|
||
}
|
||
|
||
func clearAllPendingAutoStartOnStartup() error {
|
||
pendingAutoStartMu.Lock()
|
||
defer pendingAutoStartMu.Unlock()
|
||
|
||
dir := filepath.Join("data", "pending-autostart")
|
||
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
if errors.Is(err, os.ErrNotExist) {
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
if entry.IsDir() {
|
||
continue
|
||
}
|
||
|
||
name := strings.TrimSpace(entry.Name())
|
||
if name == "" {
|
||
continue
|
||
}
|
||
if !strings.HasSuffix(strings.ToLower(name), ".json") {
|
||
continue
|
||
}
|
||
|
||
path := filepath.Join(dir, name)
|
||
|
||
raw, err := os.ReadFile(path)
|
||
if err != nil {
|
||
if errors.Is(err, os.ErrNotExist) {
|
||
continue
|
||
}
|
||
return err
|
||
}
|
||
|
||
var f pendingAutoStartFile
|
||
if err := json.Unmarshal(raw, &f); err != nil {
|
||
_ = os.Remove(path)
|
||
continue
|
||
}
|
||
|
||
kept := make([]PendingAutoStartItem, 0, len(f.Items))
|
||
for _, it := range f.Items {
|
||
if normalizePendingSourceServer(it.Source) == "resume" {
|
||
kept = append(kept, it)
|
||
}
|
||
}
|
||
|
||
if len(kept) == 0 {
|
||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||
return err
|
||
}
|
||
continue
|
||
}
|
||
|
||
if err := savePendingAutoStartItems(strings.TrimSuffix(name, filepath.Ext(name)), kept); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Startet watched+online(public) automatisch – unabhängig vom Frontend
|
||
func startChaturbateAutoStartWorker(store *ModelStore) {
|
||
if store == nil {
|
||
if verboseLogs() {
|
||
appLogln("⚠️ [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{}
|
||
probeCursor := 0
|
||
|
||
scanTicker := time.NewTicker(scanInterval)
|
||
startTicker := time.NewTicker(startInterval)
|
||
defer scanTicker.Stop()
|
||
defer startTicker.Stop()
|
||
|
||
var lastCookieHdr string
|
||
|
||
for {
|
||
select {
|
||
case <-scanTicker.C:
|
||
autostartPaused := isAutostartPaused()
|
||
|
||
s := getSettings()
|
||
if !s.UseChaturbateAPI {
|
||
queue = queue[:0]
|
||
queued = map[string]bool{}
|
||
lastCookieHdr = ""
|
||
|
||
pendingAutoStartMu.Lock()
|
||
_ = saveWatchedPendingAutoStartItemsForProvider("chaturbate", nil)
|
||
pendingAutoStartMu.Unlock()
|
||
|
||
continue
|
||
}
|
||
|
||
cookieHdr := cookieHeaderFromSettings(s)
|
||
if !hasChaturbateCookies(cookieHdr) {
|
||
lastCookieHdr = ""
|
||
continue
|
||
}
|
||
lastCookieHdr = cookieHdr
|
||
|
||
rooms, fetchedAt, fresh := getChaturbateRoomsSnapshot(snapshotMaxAge)
|
||
if !fresh {
|
||
if verboseLogs() {
|
||
if fetchedAt.IsZero() {
|
||
appLogln("⚠️ [autostart] chaturbate snapshot missing")
|
||
} else {
|
||
appLogln("⚠️ [autostart] chaturbate snapshot stale:", time.Since(fetchedAt).Round(time.Second))
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
showByUser := map[string]string{}
|
||
imageByUser := map[string]string{}
|
||
|
||
for _, r := range rooms {
|
||
u := normUser(r.Username)
|
||
if u == "" {
|
||
continue
|
||
}
|
||
showByUser[u] = normalizePendingShowServer(r.CurrentShow)
|
||
imageByUser[u] = selectBestRoomImageURL(r)
|
||
}
|
||
|
||
pendingAutoStartMu.Lock()
|
||
manualItems, err := loadManualPendingAutoStartItemsForProvider("chaturbate")
|
||
pendingAutoStartMu.Unlock()
|
||
if err != nil {
|
||
if verboseLogs() {
|
||
appLogln("⚠️ [autostart] load manual chaturbate pending failed:", err)
|
||
}
|
||
continue
|
||
}
|
||
|
||
pendingAutoStartMu.Lock()
|
||
resumeItems, err := loadResumePendingAutoStartItemsForProvider("chaturbate")
|
||
pendingAutoStartMu.Unlock()
|
||
if err != nil {
|
||
if verboseLogs() {
|
||
appLogln("⚠️ [autostart] load resume chaturbate pending failed:", err)
|
||
}
|
||
continue
|
||
}
|
||
|
||
manualByUser := map[string]PendingAutoStartItem{}
|
||
manualOrder := make([]string, 0, len(manualItems))
|
||
|
||
for _, it := range manualItems {
|
||
key := normUser(it.ModelKey)
|
||
if key == "" {
|
||
key = chaturbateUserFromURL(it.URL)
|
||
}
|
||
if key == "" {
|
||
continue
|
||
}
|
||
|
||
it.ModelKey = key
|
||
it.URL = strings.TrimSpace(it.URL)
|
||
if it.URL == "" {
|
||
it.URL = fmt.Sprintf("https://chaturbate.com/%s/", key)
|
||
}
|
||
|
||
if _, exists := manualByUser[key]; exists {
|
||
continue
|
||
}
|
||
|
||
manualByUser[key] = it
|
||
manualOrder = append(manualOrder, key)
|
||
}
|
||
|
||
resumeByUser := map[string]PendingAutoStartItem{}
|
||
resumeOrder := make([]string, 0, len(resumeItems))
|
||
|
||
for _, it := range resumeItems {
|
||
key := normUser(it.ModelKey)
|
||
if key == "" {
|
||
key = chaturbateUserFromURL(it.URL)
|
||
}
|
||
if key == "" {
|
||
continue
|
||
}
|
||
|
||
it.ModelKey = key
|
||
it.URL = strings.TrimSpace(it.URL)
|
||
if it.URL == "" {
|
||
it.URL = fmt.Sprintf("https://chaturbate.com/%s/", key)
|
||
}
|
||
|
||
if _, exists := resumeByUser[key]; exists {
|
||
continue
|
||
}
|
||
|
||
resumeByUser[key] = it
|
||
resumeOrder = append(resumeOrder, key)
|
||
}
|
||
|
||
// laufende Jobs sammeln
|
||
runningByUser := map[string]*RecordJob{}
|
||
hiddenProbeUser := ""
|
||
|
||
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 == "" {
|
||
continue
|
||
}
|
||
|
||
runningByUser[u] = j
|
||
|
||
if j.Hidden && hiddenProbeUser == "" {
|
||
hiddenProbeUser = u
|
||
}
|
||
}
|
||
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{}
|
||
watchedOrder := make([]string, 0, len(watched))
|
||
|
||
for _, m := range watched {
|
||
if !m.Watching {
|
||
continue
|
||
}
|
||
|
||
host := strings.ToLower(strings.TrimSpace(m.Host))
|
||
host = strings.TrimPrefix(host, "www.")
|
||
|
||
if host != "" && host != "chaturbate.com" {
|
||
continue
|
||
}
|
||
|
||
key := watchedChaturbateUserKey(m)
|
||
if key == "" {
|
||
continue
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if _, exists := watchedByUser[key]; exists {
|
||
continue
|
||
}
|
||
|
||
watchedByUser[key] = m
|
||
watchedOrder = append(watchedOrder, key)
|
||
}
|
||
|
||
// queue prune
|
||
nextQueue := queue[:0]
|
||
nextQueued := map[string]bool{}
|
||
|
||
for _, it := range queue {
|
||
if runningByUser[it.userKey] != nil {
|
||
continue
|
||
}
|
||
|
||
switch it.source {
|
||
case "manual":
|
||
m, ok := manualByUser[it.userKey]
|
||
if !ok {
|
||
continue
|
||
}
|
||
|
||
it.url = strings.TrimSpace(m.URL)
|
||
if it.url == "" {
|
||
continue
|
||
}
|
||
|
||
case "resume":
|
||
m, ok := resumeByUser[it.userKey]
|
||
if !ok {
|
||
continue
|
||
}
|
||
|
||
it.url = strings.TrimSpace(m.URL)
|
||
if it.url == "" {
|
||
continue
|
||
}
|
||
|
||
it.force = true
|
||
|
||
default:
|
||
m, ok := watchedByUser[it.userKey]
|
||
if !ok {
|
||
continue
|
||
}
|
||
|
||
it.url = resolveChaturbateURL(m)
|
||
if it.url == "" {
|
||
continue
|
||
}
|
||
}
|
||
|
||
show := normalizePendingShowServer(showByUser[it.userKey])
|
||
|
||
if it.blind {
|
||
if show != "offline" && show != "unknown" {
|
||
continue
|
||
}
|
||
} else {
|
||
if show != "public" {
|
||
continue
|
||
}
|
||
}
|
||
|
||
nextQueue = append(nextQueue, it)
|
||
nextQueued[it.userKey] = true
|
||
}
|
||
|
||
queue = nextQueue
|
||
queued = nextQueued
|
||
|
||
blindQueued := false
|
||
selectedBlindUser := hiddenProbeUser
|
||
|
||
for _, it := range queue {
|
||
if it.blind {
|
||
blindQueued = true
|
||
if selectedBlindUser == "" {
|
||
selectedBlindUser = it.userKey
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
offlineCandidates := make([]autoStartItem, 0, len(watchedOrder))
|
||
nextPending := make([]PendingAutoStartItem, 0, len(watchedOrder)+len(resumeOrder)+len(runningByUser))
|
||
now := time.Now()
|
||
|
||
resumePendingThisScan := map[string]bool{}
|
||
|
||
// Sichtbare laufende Downloads bei private/hidden/away stoppen
|
||
// und als "resume" merken. Diese Resume-Einträge starten später
|
||
// unabhängig von Autostart-Pause und unabhängig vom Download-Limit.
|
||
for user, runningJob := range runningByUser {
|
||
if runningJob == nil {
|
||
continue
|
||
}
|
||
if runningJob.Hidden {
|
||
continue
|
||
}
|
||
|
||
show := normalizePendingShowServer(showByUser[user])
|
||
if show != "private" && show != "hidden" && show != "away" {
|
||
continue
|
||
}
|
||
|
||
u := strings.TrimSpace(runningJob.SourceURL)
|
||
if u == "" {
|
||
u = fmt.Sprintf("https://chaturbate.com/%s/", user)
|
||
}
|
||
|
||
img := strings.TrimSpace(imageByUser[user])
|
||
|
||
nextPending = append(nextPending, PendingAutoStartItem{
|
||
ModelKey: user,
|
||
URL: u,
|
||
Mode: "wait_public",
|
||
CurrentShow: show,
|
||
ImageURL: img,
|
||
Source: "resume",
|
||
})
|
||
|
||
resumePendingThisScan[user] = true
|
||
|
||
if verboseLogs() {
|
||
appLogln("⏸️ [autostart] stopped because model is no longer public:", user, show)
|
||
}
|
||
|
||
stopJobsInternal([]*RecordJob{runningJob})
|
||
}
|
||
|
||
// Resume hat Vorrang und ignoriert Autostart-Pause.
|
||
for _, user := range resumeOrder {
|
||
itm := resumeByUser[user]
|
||
|
||
u := strings.TrimSpace(itm.URL)
|
||
if u == "" {
|
||
u = fmt.Sprintf("https://chaturbate.com/%s/", user)
|
||
}
|
||
|
||
show := normalizePendingShowServer(showByUser[user])
|
||
img := strings.TrimSpace(imageByUser[user])
|
||
|
||
switch show {
|
||
case "public":
|
||
if runningByUser[user] != nil {
|
||
continue
|
||
}
|
||
if queued[user] {
|
||
continue
|
||
}
|
||
|
||
queue = append(queue, autoStartItem{
|
||
userKey: user,
|
||
url: u,
|
||
blind: false,
|
||
source: "resume",
|
||
force: true,
|
||
})
|
||
queued[user] = true
|
||
|
||
case "private", "hidden", "away", "offline", "unknown":
|
||
if resumePendingThisScan[user] {
|
||
continue
|
||
}
|
||
|
||
nextPending = append(nextPending, PendingAutoStartItem{
|
||
ModelKey: user,
|
||
URL: u,
|
||
Mode: "wait_public",
|
||
CurrentShow: show,
|
||
ImageURL: img,
|
||
Source: "resume",
|
||
})
|
||
}
|
||
}
|
||
|
||
for _, user := range watchedOrder {
|
||
m := watchedByUser[user]
|
||
|
||
u := resolveChaturbateURL(m)
|
||
if u == "" {
|
||
continue
|
||
}
|
||
|
||
show := normalizePendingShowServer(showByUser[user])
|
||
img := strings.TrimSpace(imageByUser[user])
|
||
|
||
switch show {
|
||
case "public":
|
||
if autostartPaused {
|
||
continue
|
||
}
|
||
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,
|
||
blind: false,
|
||
source: "watched",
|
||
})
|
||
queued[user] = true
|
||
|
||
case "private", "hidden", "away":
|
||
nextPending = append(nextPending, PendingAutoStartItem{
|
||
ModelKey: user,
|
||
URL: u,
|
||
Mode: "wait_public",
|
||
CurrentShow: show,
|
||
ImageURL: img,
|
||
Source: "watched",
|
||
})
|
||
|
||
default:
|
||
if autostartPaused {
|
||
continue
|
||
}
|
||
if runningByUser[user] != nil {
|
||
continue
|
||
}
|
||
if queued[user] {
|
||
continue
|
||
}
|
||
if t, ok := lastBlindTry[user]; ok && now.Sub(t) < blindRetryCooldown {
|
||
continue
|
||
}
|
||
|
||
offlineCandidates = append(offlineCandidates, autoStartItem{
|
||
userKey: user,
|
||
url: u,
|
||
blind: true,
|
||
source: "watched",
|
||
})
|
||
}
|
||
}
|
||
|
||
for _, user := range manualOrder {
|
||
if _, exists := watchedByUser[user]; exists {
|
||
// watched gewinnt
|
||
continue
|
||
}
|
||
|
||
itm := manualByUser[user]
|
||
u := strings.TrimSpace(itm.URL)
|
||
if u == "" {
|
||
continue
|
||
}
|
||
|
||
show := normalizePendingShowServer(showByUser[user])
|
||
|
||
switch show {
|
||
case "public":
|
||
if autostartPaused {
|
||
continue
|
||
}
|
||
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,
|
||
blind: false,
|
||
source: "manual",
|
||
})
|
||
queued[user] = true
|
||
|
||
case "private", "hidden", "away":
|
||
// manual pending bleibt einfach stehen und wartet auf public
|
||
continue
|
||
|
||
default:
|
||
if autostartPaused {
|
||
continue
|
||
}
|
||
if runningByUser[user] != nil {
|
||
continue
|
||
}
|
||
if queued[user] {
|
||
continue
|
||
}
|
||
if t, ok := lastBlindTry[user]; ok && now.Sub(t) < blindRetryCooldown {
|
||
continue
|
||
}
|
||
|
||
offlineCandidates = append(offlineCandidates, autoStartItem{
|
||
userKey: user,
|
||
url: u,
|
||
blind: true,
|
||
source: "manual",
|
||
})
|
||
}
|
||
}
|
||
|
||
// Nur EIN Offline-Kandidat gleichzeitig in die Queue.
|
||
// Bei pausiertem Autostart werden oben keine normalen Offline-Kandidaten erzeugt.
|
||
if !blindQueued && !hasHiddenProbeRunningForProvider("chaturbate") && len(offlineCandidates) > 0 {
|
||
n := len(offlineCandidates)
|
||
start := 0
|
||
if probeCursor > 0 {
|
||
start = probeCursor % n
|
||
}
|
||
|
||
for i := 0; i < n; i++ {
|
||
idx := (start + i) % n
|
||
it := offlineCandidates[idx]
|
||
|
||
if queued[it.userKey] {
|
||
continue
|
||
}
|
||
if runningByUser[it.userKey] != nil {
|
||
continue
|
||
}
|
||
|
||
queue = append(queue, it)
|
||
queued[it.userKey] = true
|
||
selectedBlindUser = it.userKey
|
||
probeCursor = (idx + 1) % n
|
||
break
|
||
}
|
||
}
|
||
|
||
if selectedBlindUser != "" && !autostartPaused {
|
||
if m, ok := watchedByUser[selectedBlindUser]; ok {
|
||
u := resolveChaturbateURL(m)
|
||
if u != "" {
|
||
show := normalizePendingShowServer(showByUser[selectedBlindUser])
|
||
img := strings.TrimSpace(imageByUser[selectedBlindUser])
|
||
|
||
nextProbeAtMs := now.Add(blindRetryCooldown).UnixMilli()
|
||
if t, ok := lastBlindTry[selectedBlindUser]; ok {
|
||
nextProbeAtMs = t.Add(blindRetryCooldown).UnixMilli()
|
||
}
|
||
|
||
nextPending = append(nextPending, PendingAutoStartItem{
|
||
ModelKey: selectedBlindUser,
|
||
URL: u,
|
||
Mode: "probe_retry",
|
||
NextProbeAtMs: nextProbeAtMs,
|
||
CurrentShow: show,
|
||
ImageURL: img,
|
||
Source: "watched",
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
pendingAutoStartMu.Lock()
|
||
_ = saveWatchedPendingAutoStartItemsForProvider("chaturbate", nextPending)
|
||
pendingAutoStartMu.Unlock()
|
||
|
||
case <-startTicker.C:
|
||
s := getSettings()
|
||
if !s.UseChaturbateAPI {
|
||
continue
|
||
}
|
||
|
||
if !hasChaturbateCookies(lastCookieHdr) {
|
||
continue
|
||
}
|
||
|
||
if len(queue) == 0 {
|
||
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))
|
||
}
|
||
|
||
paused := isAutostartPaused()
|
||
|
||
startIdx := -1
|
||
for i, candidate := range queue {
|
||
if paused && !candidate.force {
|
||
continue
|
||
}
|
||
|
||
startIdx = i
|
||
break
|
||
}
|
||
|
||
if startIdx < 0 {
|
||
continue
|
||
}
|
||
|
||
it := queue[startIdx]
|
||
queue = append(queue[:startIdx], queue[startIdx+1:]...)
|
||
delete(queued, it.userKey)
|
||
|
||
show := strings.TrimSpace(showByUser[it.userKey])
|
||
isPublic := strings.Contains(show, "public")
|
||
|
||
// Nicht-public nur einzeln nacheinander prüfen.
|
||
if it.blind && hasHiddenProbeRunningForProvider("chaturbate") {
|
||
continue
|
||
}
|
||
|
||
if isJobRunningForURL(it.url) {
|
||
continue
|
||
}
|
||
|
||
if !it.blind && isPublic {
|
||
lastTry[it.userKey] = time.Now()
|
||
|
||
job, err := startRecordingInternal(RecordRequest{
|
||
URL: it.url,
|
||
Cookie: lastCookieHdr,
|
||
IgnoreConcurrentLimit: it.force,
|
||
})
|
||
if err != nil {
|
||
if verboseLogs() {
|
||
appLogln("❌ [autostart] start failed:", it.url, err)
|
||
}
|
||
continue
|
||
}
|
||
|
||
if verboseLogs() {
|
||
if it.source == "resume" {
|
||
appLogln("▶️ [autostart] resumed:", it.url)
|
||
} else {
|
||
appLogln("▶️ [autostart] started:", it.url)
|
||
}
|
||
}
|
||
|
||
if job != nil {
|
||
if it.source == "resume" {
|
||
pendingAutoStartMu.Lock()
|
||
_ = removeResumePendingAutoStartItemForProvider("chaturbate", it.userKey)
|
||
pendingAutoStartMu.Unlock()
|
||
|
||
go chaturbateAbortIfNoOutput(job.ID, outputProbeMax, nil, func() {
|
||
pendingAutoStartMu.Lock()
|
||
_ = saveResumePendingAutoStartItemForProvider("chaturbate", PendingAutoStartItem{
|
||
ModelKey: it.userKey,
|
||
URL: it.url,
|
||
Mode: "wait_public",
|
||
CurrentShow: "unknown",
|
||
Source: "resume",
|
||
})
|
||
pendingAutoStartMu.Unlock()
|
||
})
|
||
|
||
continue
|
||
}
|
||
|
||
if it.source == "manual" {
|
||
go chaturbateAbortIfNoOutput(job.ID, outputProbeMax, func() {
|
||
pendingAutoStartMu.Lock()
|
||
_ = removeManualPendingAutoStartItemForProvider("chaturbate", it.userKey)
|
||
pendingAutoStartMu.Unlock()
|
||
}, nil)
|
||
} else {
|
||
pendingAutoStartMu.Lock()
|
||
_ = removeWatchedPendingAutoStartItemForProvider("chaturbate", it.userKey)
|
||
pendingAutoStartMu.Unlock()
|
||
|
||
go chaturbateAbortIfNoOutput(job.ID, outputProbeMax, nil, nil)
|
||
}
|
||
}
|
||
} else {
|
||
if paused && !it.force {
|
||
continue
|
||
}
|
||
|
||
lastBlindTry[it.userKey] = time.Now()
|
||
|
||
job, err := startRecordingInternal(RecordRequest{
|
||
URL: it.url,
|
||
Cookie: lastCookieHdr,
|
||
Hidden: true,
|
||
IgnoreConcurrentLimit: it.force,
|
||
})
|
||
if err != nil || job == nil {
|
||
if verboseLogs() {
|
||
appLogln("❌ [autostart] blind start failed:", it.url, err)
|
||
}
|
||
continue
|
||
}
|
||
|
||
if verboseLogs() {
|
||
appLogln("▶️ [autostart] blind try started:", it.url)
|
||
}
|
||
|
||
if it.source == "manual" {
|
||
go chaturbateAbortIfNoOutput(job.ID, outputProbeMax, func() {
|
||
pendingAutoStartMu.Lock()
|
||
_ = removeManualPendingAutoStartItemForProvider("chaturbate", it.userKey)
|
||
pendingAutoStartMu.Unlock()
|
||
}, nil)
|
||
} else {
|
||
go chaturbateAbortIfNoOutput(job.ID, outputProbeMax, nil, nil)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|