260 lines
7.1 KiB
Go
260 lines
7.1 KiB
Go
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 = 10 * 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()
|
||
|
||
// 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)
|
||
}
|