737 lines
16 KiB
Go
737 lines
16 KiB
Go
// backend/myfreecams_online.go
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
const myfreecamsOnlineLookupURLFmt = "https://api-edge.myfreecams.com/usernameLookup/%s"
|
||
|
||
const (
|
||
mfcOnlinePollInterval = 5 * time.Minute
|
||
mfcOnlineMaxConcurrent = 1
|
||
mfcOnlinePerModelTimeout = 8 * time.Second
|
||
)
|
||
|
||
type mfcMaybeString string
|
||
|
||
func (s *mfcMaybeString) UnmarshalJSON(b []byte) error {
|
||
raw := strings.TrimSpace(string(b))
|
||
|
||
switch raw {
|
||
case "", "null", "false", "true":
|
||
*s = ""
|
||
return nil
|
||
}
|
||
|
||
var text string
|
||
if err := json.Unmarshal(b, &text); err == nil {
|
||
*s = mfcMaybeString(strings.TrimSpace(text))
|
||
return nil
|
||
}
|
||
|
||
// MFC liefert gelegentlich unerwartete Typen.
|
||
// Nicht den ganzen usernameLookup scheitern lassen.
|
||
*s = ""
|
||
return nil
|
||
}
|
||
|
||
func (s mfcMaybeString) String() string {
|
||
return strings.TrimSpace(string(s))
|
||
}
|
||
|
||
type MyFreeCamsOnlineRoomLite struct {
|
||
Username string `json:"username"`
|
||
CurrentShow string `json:"current_show"` // public/private/offline/unknown
|
||
ChatRoomURL string `json:"chat_room_url"`
|
||
ImageURL string `json:"image_url"`
|
||
|
||
Gender string `json:"gender,omitempty"`
|
||
Country string `json:"country,omitempty"`
|
||
RoomTopic string `json:"room_topic,omitempty"`
|
||
RoomCount int `json:"room_count,omitempty"`
|
||
Tags []string `json:"tags,omitempty"`
|
||
}
|
||
|
||
type myfreecamsOnlineCache struct {
|
||
LiteByUser map[string]MyFreeCamsOnlineRoomLite
|
||
|
||
FetchedAt time.Time
|
||
LastAttempt time.Time
|
||
LastErr string
|
||
}
|
||
|
||
var (
|
||
mfcOnlineHTTP = &http.Client{Timeout: mfcOnlinePerModelTimeout + 2*time.Second}
|
||
|
||
mfcOnlineMu sync.RWMutex
|
||
mfcOnline myfreecamsOnlineCache
|
||
|
||
mfcOnlineModelStore *ModelStore
|
||
)
|
||
|
||
var (
|
||
mfcOnlineRefreshMu sync.Mutex
|
||
mfcOnlineRefreshInFlight bool
|
||
)
|
||
|
||
func setMyFreeCamsOnlineModelStore(store *ModelStore) {
|
||
mfcOnlineModelStore = store
|
||
}
|
||
|
||
type mfcOnlineLookupResp struct {
|
||
Result struct {
|
||
Success int `json:"success"`
|
||
Message string `json:"message"`
|
||
|
||
User struct {
|
||
ID int `json:"id"`
|
||
UserID int `json:"user_id"`
|
||
Username string `json:"username"`
|
||
Avatar mfcMaybeString `json:"avatar"`
|
||
Tags []string `json:"tags"`
|
||
|
||
Sessions []struct {
|
||
SessionID int `json:"session_id"`
|
||
VState int `json:"vstate"`
|
||
RoomCount int `json:"room_count"`
|
||
RoomTopic string `json:"room_topic"`
|
||
ServerName string `json:"server_name"`
|
||
Phase string `json:"phase"`
|
||
SnapURL string `json:"snap_url"`
|
||
} `json:"sessions"`
|
||
|
||
Profile struct {
|
||
Gender string `json:"gender"`
|
||
Country string `json:"country"`
|
||
} `json:"profile"`
|
||
} `json:"user"`
|
||
} `json:"result"`
|
||
|
||
Err int `json:"err"`
|
||
}
|
||
|
||
func mfcOnlineDigitsOnly(s string) string {
|
||
var b strings.Builder
|
||
for _, r := range strings.TrimSpace(s) {
|
||
if r >= '0' && r <= '9' {
|
||
b.WriteRune(r)
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
func normalizeMFCOnlineImageURL(raw string) string {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return ""
|
||
}
|
||
|
||
if strings.HasPrefix(raw, "//") {
|
||
raw = "https:" + raw
|
||
} else if strings.HasPrefix(raw, "/") {
|
||
raw = "https://snap.mfcimg.com" + raw
|
||
} else if !strings.HasPrefix(raw, "http://") && !strings.HasPrefix(raw, "https://") {
|
||
raw = "https://snap.mfcimg.com/" + strings.TrimLeft(raw, "/")
|
||
}
|
||
|
||
u, err := url.Parse(raw)
|
||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||
return raw
|
||
}
|
||
|
||
q := u.Query()
|
||
q.Set("no-cache", strconv.FormatInt(time.Now().UnixNano(), 10))
|
||
u.RawQuery = q.Encode()
|
||
|
||
return u.String()
|
||
}
|
||
|
||
func buildMFCOnlineSnapimgURL(serverName string, rawUserID int) string {
|
||
serverID := mfcOnlineDigitsOnly(serverName)
|
||
if serverID == "" || rawUserID <= 0 {
|
||
return ""
|
||
}
|
||
|
||
return normalizeMFCOnlineImageURL(fmt.Sprintf(
|
||
"https://snap.mfcimg.com/snapimg/%s/853x480/mfc_%d",
|
||
serverID,
|
||
rawUserID,
|
||
))
|
||
}
|
||
|
||
func normalizeMFCOnlineRoomStatus(hasSession bool, vstate int) string {
|
||
if !hasSession {
|
||
return "offline"
|
||
}
|
||
|
||
// Deine Beispiele:
|
||
// vstate=0 => public
|
||
// vstate=14 => private show
|
||
if vstate == 0 {
|
||
return "public"
|
||
}
|
||
|
||
return "private"
|
||
}
|
||
|
||
func selectMFCOnlineImageURL(userID int, avatarURL string, sessServerName string, sessSnapURL string) string {
|
||
// Bevorzugt die stabile Live-Snap-URL:
|
||
// https://snap.mfcimg.com/snapimg/<server>/853x480/mfc_<userID>
|
||
if img := buildMFCOnlineSnapimgURL(sessServerName, userID); img != "" {
|
||
return img
|
||
}
|
||
|
||
if img := normalizeMFCOnlineImageURL(sessSnapURL); img != "" {
|
||
return img
|
||
}
|
||
|
||
return normalizeMFCOnlineImageURL(avatarURL)
|
||
}
|
||
|
||
func fetchMyFreeCamsOnlineModel(ctx context.Context, model string) (MyFreeCamsOnlineRoomLite, []byte, error) {
|
||
model = strings.TrimSpace(model)
|
||
if model == "" {
|
||
return MyFreeCamsOnlineRoomLite{}, nil, appErrorf("mfc model leer")
|
||
}
|
||
|
||
if err := mfcLookupPausedError(); err != nil {
|
||
return MyFreeCamsOnlineRoomLite{}, nil, err
|
||
}
|
||
|
||
upstreamURL := fmt.Sprintf(myfreecamsOnlineLookupURLFmt, url.PathEscape(model))
|
||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, upstreamURL, nil)
|
||
if err != nil {
|
||
return MyFreeCamsOnlineRoomLite{}, nil, err
|
||
}
|
||
|
||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
|
||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||
req.Header.Set("Referer", "https://www.myfreecams.com/#"+model)
|
||
req.Header.Set("Origin", "https://www.myfreecams.com")
|
||
|
||
resp, err := mfcOnlineHTTP.Do(req)
|
||
if err != nil {
|
||
return MyFreeCamsOnlineRoomLite{}, nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return MyFreeCamsOnlineRoomLite{}, raw, appErrorf(
|
||
"mfc usernameLookup %s HTTP %d: %s",
|
||
model,
|
||
resp.StatusCode,
|
||
strings.TrimSpace(string(raw)),
|
||
)
|
||
}
|
||
|
||
var data mfcOnlineLookupResp
|
||
if err := json.Unmarshal(raw, &data); err != nil {
|
||
return MyFreeCamsOnlineRoomLite{}, raw, appErrorf("mfc usernameLookup json %s: %w", model, err)
|
||
}
|
||
|
||
user := data.Result.User
|
||
avatarURL := user.Avatar.String()
|
||
username := strings.TrimSpace(user.Username)
|
||
if username == "" {
|
||
username = model
|
||
}
|
||
|
||
if data.Result.Success == 0 {
|
||
msg := strings.TrimSpace(data.Result.Message)
|
||
if msg == "" {
|
||
msg = "lookup failed"
|
||
}
|
||
|
||
if isMFCServersBusyMessage(msg) {
|
||
pauseMFCLookups(msg, mfcLookupBusyCooldown)
|
||
}
|
||
|
||
return MyFreeCamsOnlineRoomLite{}, raw, appErrorf(
|
||
"mfc usernameLookup %s: %s",
|
||
model,
|
||
msg,
|
||
)
|
||
}
|
||
|
||
if user.ID <= 0 {
|
||
return MyFreeCamsOnlineRoomLite{}, raw, appErrorf(
|
||
"mfc usernameLookup %s: missing user.id",
|
||
model,
|
||
)
|
||
}
|
||
|
||
hasSession := len(user.Sessions) > 0
|
||
|
||
var (
|
||
vstate int
|
||
roomCount int
|
||
roomTopic string
|
||
serverName string
|
||
snapURL string
|
||
)
|
||
|
||
if hasSession {
|
||
sess := user.Sessions[0]
|
||
vstate = sess.VState
|
||
roomCount = sess.RoomCount
|
||
roomTopic = strings.TrimSpace(sess.RoomTopic)
|
||
serverName = strings.TrimSpace(sess.ServerName)
|
||
snapURL = strings.TrimSpace(sess.SnapURL)
|
||
}
|
||
|
||
status := normalizeMFCOnlineRoomStatus(hasSession, vstate)
|
||
imageURL := selectMFCOnlineImageURL(user.ID, avatarURL, serverName, snapURL)
|
||
|
||
return MyFreeCamsOnlineRoomLite{
|
||
Username: username,
|
||
CurrentShow: status,
|
||
ChatRoomURL: "https://www.myfreecams.com/#" + username,
|
||
ImageURL: imageURL,
|
||
Gender: strings.TrimSpace(user.Profile.Gender),
|
||
Country: strings.TrimSpace(user.Profile.Country),
|
||
RoomTopic: roomTopic,
|
||
RoomCount: roomCount,
|
||
Tags: user.Tags,
|
||
}, raw, nil
|
||
}
|
||
|
||
func listMyFreeCamsStoreModels(store *ModelStore) []StoredModel {
|
||
if store == nil {
|
||
return nil
|
||
}
|
||
|
||
all := store.List()
|
||
out := make([]StoredModel, 0, len(all))
|
||
seen := map[string]bool{}
|
||
|
||
for _, m := range all {
|
||
host := canonicalHost(m.Host)
|
||
if host != "myfreecams.com" {
|
||
continue
|
||
}
|
||
|
||
modelKey := strings.TrimSpace(m.ModelKey)
|
||
if modelKey == "" {
|
||
continue
|
||
}
|
||
|
||
k := strings.ToLower(modelKey)
|
||
if seen[k] {
|
||
continue
|
||
}
|
||
seen[k] = true
|
||
|
||
out = append(out, m)
|
||
}
|
||
|
||
return out
|
||
}
|
||
|
||
func refreshMyFreeCamsSnapshotNow(ctx context.Context, store *ModelStore) (time.Time, int, error) {
|
||
if store == nil {
|
||
return time.Time{}, 0, appErrorf("mfc online: model store nil")
|
||
}
|
||
|
||
mfcOnlineMu.Lock()
|
||
mfcOnline.LastAttempt = time.Now()
|
||
mfcOnlineMu.Unlock()
|
||
|
||
models := listMyFreeCamsStoreModels(store)
|
||
fetchedAt := time.Now().UTC()
|
||
|
||
if err := mfcLookupPausedError(); err != nil {
|
||
mfcOnlineMu.Lock()
|
||
mfcOnline.LastAttempt = time.Now()
|
||
mfcOnline.LastErr = err.Error()
|
||
mfcOnlineMu.Unlock()
|
||
|
||
return fetchedAt, 0, err
|
||
}
|
||
|
||
liteByUser := make(map[string]MyFreeCamsOnlineRoomLite, len(models))
|
||
|
||
// Vorherigen Snapshot behalten, damit einzelne Timeout-Fehler nicht sofort
|
||
// alle bekannten MFC-Online-Daten aus dem Cache löschen.
|
||
mfcOnlineMu.RLock()
|
||
for k, v := range mfcOnline.LiteByUser {
|
||
liteByUser[k] = v
|
||
}
|
||
mfcOnlineMu.RUnlock()
|
||
|
||
if len(models) == 0 {
|
||
mfcOnlineMu.Lock()
|
||
mfcOnline.LiteByUser = liteByUser
|
||
mfcOnline.FetchedAt = fetchedAt
|
||
mfcOnline.LastErr = ""
|
||
mfcOnlineMu.Unlock()
|
||
|
||
return fetchedAt, 0, nil
|
||
}
|
||
|
||
type result struct {
|
||
model StoredModel
|
||
room MyFreeCamsOnlineRoomLite
|
||
raw []byte
|
||
err error
|
||
}
|
||
|
||
sem := make(chan struct{}, mfcOnlineMaxConcurrent)
|
||
resCh := make(chan result, len(models))
|
||
|
||
var wg sync.WaitGroup
|
||
|
||
for _, m := range models {
|
||
m := m
|
||
wg.Add(1)
|
||
|
||
go func() {
|
||
defer wg.Done()
|
||
|
||
select {
|
||
case <-ctx.Done():
|
||
resCh <- result{model: m, err: ctx.Err()}
|
||
return
|
||
case sem <- struct{}{}:
|
||
}
|
||
defer func() { <-sem }()
|
||
|
||
if err := mfcLookupPausedError(); err != nil {
|
||
resCh <- result{model: m, err: err}
|
||
return
|
||
}
|
||
|
||
reqCtx, reqCancel := context.WithTimeout(ctx, mfcOnlinePerModelTimeout)
|
||
room, raw, err := fetchMyFreeCamsOnlineModel(reqCtx, m.ModelKey)
|
||
reqCancel()
|
||
resCh <- result{
|
||
model: m,
|
||
room: room,
|
||
raw: raw,
|
||
err: err,
|
||
}
|
||
}()
|
||
}
|
||
|
||
wg.Wait()
|
||
close(resCh)
|
||
|
||
var (
|
||
firstErr error
|
||
okCount int
|
||
failedCount int
|
||
timeoutCount int
|
||
)
|
||
|
||
for res := range resCh {
|
||
modelKey := strings.TrimSpace(res.model.ModelKey)
|
||
if modelKey == "" {
|
||
continue
|
||
}
|
||
|
||
if res.err != nil {
|
||
failedCount++
|
||
|
||
if errors.Is(res.err, context.DeadlineExceeded) {
|
||
timeoutCount++
|
||
}
|
||
|
||
if firstErr == nil {
|
||
firstErr = res.err
|
||
}
|
||
|
||
// Kein Log pro Model mehr, sonst spammt es bei vielen Timeouts.
|
||
continue
|
||
}
|
||
|
||
room := res.room
|
||
if strings.TrimSpace(room.Username) == "" {
|
||
room.Username = modelKey
|
||
}
|
||
|
||
key := strings.ToLower(strings.TrimSpace(room.Username))
|
||
if key == "" {
|
||
key = strings.ToLower(modelKey)
|
||
}
|
||
|
||
liteByUser[key] = room
|
||
okCount++
|
||
|
||
isOnline := room.CurrentShow != "offline" && room.CurrentShow != "unknown"
|
||
|
||
_ = store.SetChaturbateRoomState(
|
||
"myfreecams.com",
|
||
modelKey,
|
||
room.CurrentShow,
|
||
isOnline,
|
||
room.ChatRoomURL,
|
||
room.ImageURL,
|
||
fetchedAt,
|
||
)
|
||
|
||
_ = store.SetLastSeenOnline(
|
||
"myfreecams.com",
|
||
modelKey,
|
||
isOnline,
|
||
fetchedAt.Format(time.RFC3339Nano),
|
||
)
|
||
|
||
if len(res.raw) > 0 {
|
||
_ = store.SetBioContext(
|
||
"myfreecams.com",
|
||
modelKey,
|
||
string(res.raw),
|
||
fetchedAt.Format(time.RFC3339Nano),
|
||
)
|
||
}
|
||
|
||
if strings.TrimSpace(room.ImageURL) != "" {
|
||
_ = store.SetProfileImageURLOnly(
|
||
"myfreecams.com",
|
||
modelKey,
|
||
room.ImageURL,
|
||
fetchedAt.Format(time.RFC3339Nano),
|
||
)
|
||
}
|
||
}
|
||
|
||
lastErr := ""
|
||
if firstErr != nil {
|
||
lastErr = fmt.Sprintf(
|
||
"%d/%d lookups failed (%d timeouts); first error: %v",
|
||
failedCount,
|
||
len(models),
|
||
timeoutCount,
|
||
firstErr,
|
||
)
|
||
|
||
appLogln("⚠️ [myfreecams] online refresh partial:", lastErr)
|
||
}
|
||
|
||
mfcOnlineMu.Lock()
|
||
mfcOnline.LiteByUser = liteByUser
|
||
mfcOnline.FetchedAt = fetchedAt
|
||
mfcOnline.LastErr = lastErr
|
||
mfcOnlineMu.Unlock()
|
||
|
||
// Nur hart fehlschlagen, wenn wirklich gar kein Model erfolgreich geprüft wurde.
|
||
if okCount == 0 && firstErr != nil {
|
||
return fetchedAt, okCount, firstErr
|
||
}
|
||
|
||
return fetchedAt, okCount, nil
|
||
}
|
||
|
||
func startMyFreeCamsOnlinePoller(store *ModelStore) {
|
||
const interval = mfcOnlinePollInterval
|
||
|
||
if store != nil {
|
||
setMyFreeCamsOnlineModelStore(store)
|
||
}
|
||
|
||
lastLoggedErr := ""
|
||
|
||
refreshOnce := func(trigger string) {
|
||
if !myFreeCamsProviderAPIEnabled() {
|
||
if verboseLogs() {
|
||
appLogln("ℹ️ [myfreecams] online refresh skipped: provider API disabled")
|
||
}
|
||
return
|
||
}
|
||
|
||
mfcOnlineRefreshMu.Lock()
|
||
if mfcOnlineRefreshInFlight {
|
||
mfcOnlineRefreshMu.Unlock()
|
||
return
|
||
}
|
||
mfcOnlineRefreshInFlight = true
|
||
mfcOnlineRefreshMu.Unlock()
|
||
|
||
go func() {
|
||
defer func() {
|
||
mfcOnlineRefreshMu.Lock()
|
||
mfcOnlineRefreshInFlight = false
|
||
mfcOnlineRefreshMu.Unlock()
|
||
}()
|
||
|
||
fetchedAt, count, err := refreshMyFreeCamsSnapshotNow(context.Background(), store)
|
||
|
||
if err != nil {
|
||
msg := err.Error()
|
||
|
||
if strings.Contains(msg, "usernameLookup pausiert") {
|
||
if verboseLogs() && msg != lastLoggedErr {
|
||
appLogln("⏸️ [myfreecams] online refresh skipped:", msg)
|
||
lastLoggedErr = msg
|
||
}
|
||
return
|
||
}
|
||
|
||
if msg != lastLoggedErr {
|
||
appLogln("⚠️ [myfreecams] online refresh failed:", msg)
|
||
lastLoggedErr = msg
|
||
}
|
||
return
|
||
}
|
||
|
||
if lastLoggedErr != "" {
|
||
appLogln("✅ [myfreecams] online refresh recovered")
|
||
lastLoggedErr = ""
|
||
}
|
||
|
||
if verboseLogs() {
|
||
appLogln(
|
||
"✅ [myfreecams] checked models:",
|
||
count,
|
||
"snapshot",
|
||
fetchedAt.Format(time.RFC3339),
|
||
"trigger="+trigger,
|
||
)
|
||
}
|
||
}()
|
||
}
|
||
|
||
refreshOnce("startup")
|
||
|
||
ticker := time.NewTicker(interval)
|
||
defer ticker.Stop()
|
||
|
||
for range ticker.C {
|
||
refreshOnce("ticker")
|
||
}
|
||
}
|
||
|
||
type mfcOnlineResponse struct {
|
||
Enabled bool `json:"enabled"`
|
||
FetchedAt time.Time `json:"fetchedAt"`
|
||
LastAttempt time.Time `json:"lastAttempt"`
|
||
Count int `json:"count"`
|
||
Total int `json:"total"`
|
||
LastError string `json:"lastError"`
|
||
Rooms []MyFreeCamsOnlineRoomLite `json:"rooms"`
|
||
}
|
||
|
||
func myfreecamsOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
||
http.Error(w, "Nur GET/POST erlaubt", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
|
||
wantRefresh := false
|
||
var users []string
|
||
|
||
if r.Method == http.MethodPost {
|
||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||
var req struct {
|
||
Q []string `json:"q"`
|
||
Refresh bool `json:"refresh"`
|
||
}
|
||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||
wantRefresh = req.Refresh
|
||
for _, u := range req.Q {
|
||
u = strings.ToLower(strings.TrimSpace(u))
|
||
if u != "" {
|
||
users = append(users, u)
|
||
}
|
||
}
|
||
} else {
|
||
qRefresh := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("refresh")))
|
||
wantRefresh = qRefresh == "1" || qRefresh == "true" || qRefresh == "yes"
|
||
|
||
qUsers := strings.TrimSpace(r.URL.Query().Get("q"))
|
||
if qUsers != "" {
|
||
for _, u := range strings.Split(qUsers, ",") {
|
||
u = strings.ToLower(strings.TrimSpace(u))
|
||
if u != "" {
|
||
users = append(users, u)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
store := mfcOnlineModelStore
|
||
if store == nil {
|
||
store = getModelStore()
|
||
}
|
||
|
||
if wantRefresh && store != nil {
|
||
if !myFreeCamsProviderAPIEnabled() {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
_ = json.NewEncoder(w).Encode(mfcOnlineResponse{
|
||
Enabled: false,
|
||
LastError: "MyFreeCams API ist in den Einstellungen deaktiviert",
|
||
Rooms: []MyFreeCamsOnlineRoomLite{},
|
||
})
|
||
return
|
||
}
|
||
|
||
mfcOnlineRefreshMu.Lock()
|
||
if !mfcOnlineRefreshInFlight {
|
||
mfcOnlineRefreshInFlight = true
|
||
go func() {
|
||
defer func() {
|
||
mfcOnlineRefreshMu.Lock()
|
||
mfcOnlineRefreshInFlight = false
|
||
mfcOnlineRefreshMu.Unlock()
|
||
}()
|
||
|
||
_, _, _ = refreshMyFreeCamsSnapshotNow(context.Background(), store)
|
||
}()
|
||
}
|
||
mfcOnlineRefreshMu.Unlock()
|
||
}
|
||
|
||
mfcOnlineMu.RLock()
|
||
fetchedAt := mfcOnline.FetchedAt
|
||
lastAttempt := mfcOnline.LastAttempt
|
||
lastErr := mfcOnline.LastErr
|
||
liteByUser := mfcOnline.LiteByUser
|
||
mfcOnlineMu.RUnlock()
|
||
|
||
total := 0
|
||
if liteByUser != nil {
|
||
total = len(liteByUser)
|
||
}
|
||
|
||
rooms := make([]MyFreeCamsOnlineRoomLite, 0)
|
||
|
||
if len(users) > 0 {
|
||
for _, u := range users {
|
||
if rm, ok := liteByUser[u]; ok {
|
||
rooms = append(rooms, rm)
|
||
}
|
||
}
|
||
}
|
||
|
||
if fetchedAt.IsZero() && lastErr == "" {
|
||
lastErr = "warming up"
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
|
||
_ = json.NewEncoder(w).Encode(mfcOnlineResponse{
|
||
Enabled: true,
|
||
FetchedAt: fetchedAt,
|
||
LastAttempt: lastAttempt,
|
||
Count: len(rooms),
|
||
Total: total,
|
||
LastError: lastErr,
|
||
Rooms: rooms,
|
||
})
|
||
}
|