nsfwapp/backend/chaturbate_online.go
2025-12-26 01:25:04 +01:00

264 lines
7.1 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
)
// Chaturbate Affiliates API (Online Rooms)
// https://chaturbate.com/affiliates/api/onlinerooms/?format=json&wm=827SM
const chaturbateOnlineRoomsURL = "https://chaturbate.com/affiliates/api/onlinerooms/?format=json&wm=827SM"
// ChaturbateRoom bildet die Felder ab, die die Online-Rooms API liefert.
// (Du kannst das später problemlos erweitern, wenn du weitere Felder brauchst.)
type ChaturbateRoom struct {
Gender string `json:"gender"`
Location string `json:"location"`
CurrentShow string `json:"current_show"` // public / private / hidden / away
Username string `json:"username"`
RoomSubject string `json:"room_subject"`
Tags []string `json:"tags"`
IsNew bool `json:"is_new"`
NumUsers int `json:"num_users"`
NumFollowers int `json:"num_followers"`
Country string `json:"country"`
SpokenLanguages string `json:"spoken_languages"`
DisplayName string `json:"display_name"`
Birthday string `json:"birthday"`
IsHD bool `json:"is_hd"`
Age int `json:"age"`
SecondsOnline int `json:"seconds_online"`
ImageURL string `json:"image_url"`
ImageURL360 string `json:"image_url_360x270"`
ChatRoomURL string `json:"chat_room_url"`
ChatRoomURLRS string `json:"chat_room_url_revshare"`
IframeEmbed string `json:"iframe_embed"`
IframeEmbedRS string `json:"iframe_embed_revshare"`
BlockCountries string `json:"block_from_countries"`
BlockStates string `json:"block_from_states"`
Recorded string `json:"recorded"` // kommt in der API als String "true"/"false"
Slug string `json:"slug"`
}
type chaturbateCache struct {
Rooms []ChaturbateRoom
FetchedAt time.Time
LastErr string
}
var (
cbHTTP = &http.Client{Timeout: 12 * time.Second}
cbMu sync.RWMutex
cb chaturbateCache
)
func fetchChaturbateOnlineRooms(ctx context.Context) ([]ChaturbateRoom, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, chaturbateOnlineRoomsURL, nil)
if err != nil {
return nil, err
}
// ein "normaler" UA reduziert manchmal Block/Rate-Limit Probleme
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
req.Header.Set("Accept", "application/json")
resp, err := cbHTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<10))
return nil, fmt.Errorf("chaturbate online rooms: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var rooms []ChaturbateRoom
if err := json.Unmarshal(data, &rooms); err != nil {
return nil, err
}
return rooms, nil
}
// startChaturbateOnlinePoller pollt die API alle paar Sekunden,
// aber nur, wenn der Settings-Switch "useChaturbateApi" aktiviert ist.
func startChaturbateOnlinePoller() {
const interval = 5 * time.Second
// nur loggen, wenn sich etwas ändert (sonst spammt es alle 5s)
lastLoggedCount := -1
lastLoggedErr := ""
// sofort ein initialer Tick
first := time.NewTimer(0)
defer first.Stop()
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-first.C:
case <-ticker.C:
}
if !getSettings().UseChaturbateAPI {
continue
}
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
rooms, err := fetchChaturbateOnlineRooms(ctx)
cancel()
cbMu.Lock()
if err != nil {
// ❗WICHTIG: bei Fehler NICHT fetchedAt aktualisieren,
// sonst wirkt der Cache "frisch", obwohl rooms alt sind.
cb.LastErr = err.Error()
// ❗Damit offline Models nicht hängen bleiben: rooms leeren
cb.Rooms = nil
cbMu.Unlock()
if cb.LastErr != lastLoggedErr {
fmt.Println("❌ [chaturbate] online rooms fetch failed:", cb.LastErr)
lastLoggedErr = cb.LastErr
}
continue
}
// ✅ Erfolg: komplette Liste ersetzen + fetchedAt setzen
cb.LastErr = ""
cb.Rooms = rooms
cb.FetchedAt = time.Now()
cbMu.Unlock()
cb.LastErr = ""
cb.Rooms = rooms
cbMu.Unlock()
// success logging only on changes
if lastLoggedErr != "" {
fmt.Println("✅ [chaturbate] online rooms fetch recovered")
lastLoggedErr = ""
}
if len(rooms) != lastLoggedCount {
fmt.Println("✅ [chaturbate] online rooms:", len(rooms))
lastLoggedCount = len(rooms)
}
}
}
func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Nur GET erlaubt", http.StatusMethodNotAllowed)
return
}
enabled := getSettings().UseChaturbateAPI
if !enabled {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(map[string]any{
"enabled": false,
"fetchedAt": time.Time{},
"count": 0,
"lastError": "",
"rooms": []ChaturbateRoom{},
})
return
}
// optional: ?refresh=1 triggert einen direkten Fetch (falls aktiviert)
q := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("refresh")))
wantRefresh := q == "1" || q == "true" || q == "yes"
// Snapshot des Caches
cbMu.RLock()
rooms := cb.Rooms
fetchedAt := cb.FetchedAt
lastErr := cb.LastErr
cbMu.RUnlock()
// Wenn aktiviert aber Cache noch nie gefüllt wurde, einmalig automatisch fetchen.
// (Das verhindert das "count=0 / fetchedAt=0001" Verhalten direkt nach Neustart.)
const staleAfter = 20 * time.Second
isStale := fetchedAt.IsZero() || time.Since(fetchedAt) > staleAfter
if enabled && (wantRefresh || isStale) {
ctx, cancel := context.WithTimeout(r.Context(), 12*time.Second)
freshRooms, err := fetchChaturbateOnlineRooms(ctx)
cancel()
cbMu.Lock()
if err != nil {
cb.LastErr = err.Error()
// ❗WICHTIG: keine alten rooms weitergeben
cb.Rooms = nil
// ❗FetchedAt NICHT aktualisieren (bleibt letzte erfolgreiche Zeit)
} else {
cb.LastErr = ""
cb.Rooms = freshRooms
cb.FetchedAt = time.Now()
}
rooms = cb.Rooms
fetchedAt = cb.FetchedAt
lastErr = cb.LastErr
cbMu.Unlock()
}
// nil-slice vermeiden -> Frontend bekommt [] statt null
if rooms == nil {
rooms = []ChaturbateRoom{}
}
// optional: ?show=public,private,hidden,away
showFilter := strings.TrimSpace(r.URL.Query().Get("show"))
if showFilter != "" {
allowed := map[string]bool{}
for _, s := range strings.Split(showFilter, ",") {
s = strings.ToLower(strings.TrimSpace(s))
if s != "" {
allowed[s] = true
}
}
if len(allowed) > 0 {
filtered := make([]ChaturbateRoom, 0, len(rooms))
for _, rm := range rooms {
if allowed[strings.ToLower(strings.TrimSpace(rm.CurrentShow))] {
filtered = append(filtered, rm)
}
}
rooms = filtered
}
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
// Wir liefern ein kleines Meta-Objekt, damit du im UI sofort siehst, ob der Cache aktuell ist.
out := map[string]any{
"enabled": enabled,
"fetchedAt": fetchedAt,
"count": len(rooms),
"lastError": lastErr,
"rooms": rooms,
}
_ = json.NewEncoder(w).Encode(out)
}