fixed autostart
This commit is contained in:
parent
f0541b9fe6
commit
ff341755c6
@ -217,6 +217,8 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
const scanInterval = 1 * time.Second
|
const scanInterval = 1 * time.Second
|
||||||
const startInterval = 5 * time.Second
|
const startInterval = 5 * time.Second
|
||||||
|
|
||||||
|
const snapshotMaxAge = 90 * time.Second
|
||||||
|
|
||||||
// normaler Retry für API-public
|
// normaler Retry für API-public
|
||||||
const retryCooldown = 25 * time.Second
|
const retryCooldown = 25 * time.Second
|
||||||
|
|
||||||
@ -260,10 +262,17 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
}
|
}
|
||||||
lastCookieHdr = cookieHdr
|
lastCookieHdr = cookieHdr
|
||||||
|
|
||||||
// online snapshot aus cache
|
rooms, fetchedAt, fresh := getChaturbateRoomsSnapshot(snapshotMaxAge)
|
||||||
cbMu.RLock()
|
if !fresh {
|
||||||
rooms := append([]ChaturbateRoom(nil), cb.Rooms...)
|
if verboseLogs() {
|
||||||
cbMu.RUnlock()
|
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{}
|
showByUser := map[string]string{}
|
||||||
seenInAPI := map[string]bool{}
|
seenInAPI := map[string]bool{}
|
||||||
@ -445,10 +454,10 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// unterscheiden: API-public oder Blind-Try
|
rooms, _, fresh := getChaturbateRoomsSnapshot(snapshotMaxAge)
|
||||||
cbMu.RLock()
|
if !fresh {
|
||||||
rooms := append([]ChaturbateRoom(nil), cb.Rooms...)
|
continue
|
||||||
cbMu.RUnlock()
|
}
|
||||||
|
|
||||||
showByUser := map[string]string{}
|
showByUser := map[string]string{}
|
||||||
for _, r := range rooms {
|
for _, r := range rooms {
|
||||||
|
|||||||
@ -478,14 +478,32 @@ func cbApplySnapshot(rooms []ChaturbateRoom) time.Time {
|
|||||||
return fetchedAtNow
|
return fetchedAtNow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getChaturbateRoomsSnapshot(maxAge time.Duration) ([]ChaturbateRoom, time.Time, bool) {
|
||||||
|
cbMu.RLock()
|
||||||
|
defer cbMu.RUnlock()
|
||||||
|
|
||||||
|
fetchedAt := cb.FetchedAt
|
||||||
|
if fetchedAt.IsZero() {
|
||||||
|
return nil, time.Time{}, false
|
||||||
|
}
|
||||||
|
if maxAge > 0 && time.Since(fetchedAt) > maxAge {
|
||||||
|
return nil, fetchedAt, false
|
||||||
|
}
|
||||||
|
|
||||||
|
rooms := append([]ChaturbateRoom(nil), cb.Rooms...)
|
||||||
|
return rooms, fetchedAt, true
|
||||||
|
}
|
||||||
|
|
||||||
func refreshChaturbateSnapshotNow(ctx context.Context) (time.Time, error) {
|
func refreshChaturbateSnapshotNow(ctx context.Context) (time.Time, error) {
|
||||||
|
cbMu.Lock()
|
||||||
|
cb.LastAttempt = time.Now()
|
||||||
|
cbMu.Unlock()
|
||||||
|
|
||||||
rooms, err := fetchChaturbateOnlineRooms(ctx)
|
rooms, err := fetchChaturbateOnlineRooms(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cbMu.Lock()
|
cbMu.Lock()
|
||||||
cb.LastErr = err.Error()
|
cb.LastErr = err.Error()
|
||||||
cb.Rooms = nil
|
// ✅ KEIN Cache-Reset hier
|
||||||
cb.RoomsByUser = nil
|
|
||||||
cb.LiteByUser = nil
|
|
||||||
cbMu.Unlock()
|
cbMu.Unlock()
|
||||||
return time.Time{}, err
|
return time.Time{}, err
|
||||||
}
|
}
|
||||||
@ -513,68 +531,55 @@ func startChaturbateOnlinePoller(store *ModelStore) {
|
|||||||
const interval = 10 * time.Second
|
const interval = 10 * time.Second
|
||||||
const tagsFillEvery = 10 * time.Minute
|
const tagsFillEvery = 10 * time.Minute
|
||||||
|
|
||||||
|
if store != nil {
|
||||||
|
setChaturbateOnlineModelStore(store)
|
||||||
|
}
|
||||||
|
|
||||||
lastLoggedCount := -1
|
lastLoggedCount := -1
|
||||||
lastLoggedErr := ""
|
lastLoggedErr := ""
|
||||||
|
|
||||||
var tagsMu sync.Mutex
|
var tagsMu sync.Mutex
|
||||||
var tagsLast time.Time
|
var tagsLast time.Time
|
||||||
|
|
||||||
first := time.NewTimer(0)
|
refreshOnce := func() {
|
||||||
defer first.Stop()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(interval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-first.C:
|
|
||||||
case <-ticker.C:
|
|
||||||
}
|
|
||||||
|
|
||||||
if !getSettings().UseChaturbateAPI {
|
if !getSettings().UseChaturbateAPI {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// immer merken: wir haben es versucht
|
|
||||||
cbMu.Lock()
|
|
||||||
cb.LastAttempt = time.Now()
|
|
||||||
cbMu.Unlock()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
rooms, err := fetchChaturbateOnlineRooms(ctx)
|
fetchedAtNow, err := refreshChaturbateSnapshotNow(ctx)
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cbMu.Lock()
|
cbMu.RLock()
|
||||||
cb.LastErr = err.Error()
|
msg := cb.LastErr
|
||||||
|
prevFetchedAt := cb.FetchedAt
|
||||||
|
prevCount := len(cb.Rooms)
|
||||||
|
cbMu.RUnlock()
|
||||||
|
|
||||||
// Fehler => Cache leeren (damit offline nicht hängen bleibt)
|
if msg != lastLoggedErr {
|
||||||
cb.Rooms = nil
|
if prevFetchedAt.IsZero() {
|
||||||
cb.RoomsByUser = nil
|
fmt.Println("❌ [chaturbate] online rooms fetch failed (no snapshot yet):", msg)
|
||||||
cb.LiteByUser = nil
|
} else {
|
||||||
cbMu.Unlock()
|
fmt.Printf(
|
||||||
|
"⚠️ [chaturbate] refresh failed, keeping snapshot from %s (%d rooms): %s\n",
|
||||||
if cb.LastErr != lastLoggedErr {
|
prevFetchedAt.Format(time.RFC3339),
|
||||||
fmt.Println("❌ [chaturbate] online rooms fetch failed:", cb.LastErr)
|
prevCount,
|
||||||
lastLoggedErr = cb.LastErr
|
msg,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
continue
|
lastLoggedErr = msg
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchedAtNow := cbApplySnapshot(rooms)
|
cbMu.RLock()
|
||||||
|
roomsCopy := append([]ChaturbateRoom(nil), cb.Rooms...)
|
||||||
|
roomsCount := len(cb.Rooms)
|
||||||
|
cbMu.RUnlock()
|
||||||
|
|
||||||
// ✅ Alle bekannten Chaturbate-Models in der DB mit aktuellem Online-Snapshot synchronisieren
|
// Tags nur selten und asynchron
|
||||||
if cbModelStore != nil {
|
if cbModelStore != nil && len(roomsCopy) > 0 {
|
||||||
copiedRooms := append([]ChaturbateRoom(nil), rooms...)
|
|
||||||
|
|
||||||
go func(roomsCopy []ChaturbateRoom, ts time.Time) {
|
|
||||||
syncChaturbateRoomStateIntoModelStore(cbModelStore, roomsCopy, ts)
|
|
||||||
_ = cbModelStore.SyncChaturbateOnlineForKnownModels(roomsCopy, ts)
|
|
||||||
}(copiedRooms, fetchedAtNow)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tags übernehmen ist teuer -> nur selten + im Hintergrund
|
|
||||||
if cbModelStore != nil && len(rooms) > 0 {
|
|
||||||
shouldFill := false
|
shouldFill := false
|
||||||
|
|
||||||
tagsMu.Lock()
|
tagsMu.Lock()
|
||||||
@ -585,20 +590,34 @@ func startChaturbateOnlinePoller(store *ModelStore) {
|
|||||||
tagsMu.Unlock()
|
tagsMu.Unlock()
|
||||||
|
|
||||||
if shouldFill {
|
if shouldFill {
|
||||||
go cbModelStore.FillMissingTagsFromChaturbateOnline(rooms)
|
go cbModelStore.FillMissingTagsFromChaturbateOnline(roomsCopy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// success logging only on changes
|
|
||||||
if lastLoggedErr != "" {
|
if lastLoggedErr != "" {
|
||||||
fmt.Println("✅ [chaturbate] online rooms fetch recovered")
|
fmt.Println("✅ [chaturbate] online rooms fetch recovered")
|
||||||
lastLoggedErr = ""
|
lastLoggedErr = ""
|
||||||
}
|
}
|
||||||
if verboseLogs() && len(rooms) != lastLoggedCount {
|
|
||||||
fmt.Println("✅ [chaturbate] online rooms:", len(rooms))
|
if verboseLogs() && roomsCount != lastLoggedCount {
|
||||||
lastLoggedCount = len(rooms)
|
fmt.Printf(
|
||||||
|
"✅ [chaturbate] online rooms: %d (snapshot %s)\n",
|
||||||
|
roomsCount,
|
||||||
|
fetchedAtNow.Format(time.RFC3339),
|
||||||
|
)
|
||||||
|
lastLoggedCount = roomsCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ sofort einmal versuchen
|
||||||
|
refreshOnce()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
refreshOnce()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var onlineCacheMu sync.Mutex
|
var onlineCacheMu sync.Mutex
|
||||||
|
|||||||
@ -277,13 +277,51 @@ func (pq *PostWorkQueue) StatusForKey(key string) PostWorkKeyStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// global (oder in deinem app struct halten)
|
// global (oder in deinem app struct halten)
|
||||||
var postWorkQ = NewPostWorkQueue(512, 6) // schneller kritischer Pfad
|
var postWorkQ = NewPostWorkQueue(512, 2) // schneller kritischer Pfad
|
||||||
var enrichQ = NewPostWorkQueue(512, 2) // langsame Hintergrundaufgaben
|
var enrichQ = NewPostWorkQueue(512, 1) // langsame Hintergrundaufgaben
|
||||||
|
|
||||||
// --- Status Refresher (ehemals postwork_refresh.go) ---
|
// --- Status Refresher (ehemals postwork_refresh.go) ---
|
||||||
|
|
||||||
|
type enrichStatusSnapshot struct {
|
||||||
|
State string
|
||||||
|
Position int
|
||||||
|
Waiting int
|
||||||
|
Running int
|
||||||
|
MaxParallel int
|
||||||
|
}
|
||||||
|
|
||||||
|
var enrichStatusLastMu sync.Mutex
|
||||||
|
var enrichStatusLast = map[string]enrichStatusSnapshot{}
|
||||||
|
|
||||||
|
func shouldPublishEnrichStatus(key string, st PostWorkKeyStatus) bool {
|
||||||
|
enrichStatusLastMu.Lock()
|
||||||
|
defer enrichStatusLastMu.Unlock()
|
||||||
|
|
||||||
|
cur := enrichStatusSnapshot{
|
||||||
|
State: st.State,
|
||||||
|
Position: st.Position,
|
||||||
|
Waiting: st.Waiting,
|
||||||
|
Running: st.Running,
|
||||||
|
MaxParallel: st.MaxParallel,
|
||||||
|
}
|
||||||
|
|
||||||
|
prev, ok := enrichStatusLast[key]
|
||||||
|
if ok && prev == cur {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
enrichStatusLast[key] = cur
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearEnrichStatusSnapshot(key string) {
|
||||||
|
enrichStatusLastMu.Lock()
|
||||||
|
delete(enrichStatusLast, key)
|
||||||
|
enrichStatusLastMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func startEnrichStatusRefresher() {
|
func startEnrichStatusRefresher() {
|
||||||
t := time.NewTicker(1 * time.Second)
|
t := time.NewTicker(2 * time.Second)
|
||||||
go func() {
|
go func() {
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|
||||||
@ -318,6 +356,11 @@ func startEnrichStatusRefresher() {
|
|||||||
|
|
||||||
// Nur senden, wenn der Job tatsächlich gerade queued/running ist
|
// Nur senden, wenn der Job tatsächlich gerade queued/running ist
|
||||||
if st.State != "queued" && st.State != "running" {
|
if st.State != "queued" && st.State != "running" {
|
||||||
|
clearEnrichStatusSnapshot(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldPublishEnrichStatus(key, st) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2003,7 +2003,7 @@ func generateTeaserChunkMP4(ctx context.Context, src, out string, start, dur flo
|
|||||||
"-level", "4.2",
|
"-level", "4.2",
|
||||||
"-preset", opts.Preset,
|
"-preset", opts.Preset,
|
||||||
"-crf", strconv.Itoa(opts.CRF),
|
"-crf", strconv.Itoa(opts.CRF),
|
||||||
"-threads", "4",
|
"-threads", "2",
|
||||||
)
|
)
|
||||||
|
|
||||||
if opts.UseVsync2 {
|
if opts.UseVsync2 {
|
||||||
@ -2145,7 +2145,7 @@ func generateTeaserPreviewMP4WithProgress(ctx context.Context, srcPath, outPath
|
|||||||
"-level", "4.2",
|
"-level", "4.2",
|
||||||
"-preset", opts.Preset,
|
"-preset", opts.Preset,
|
||||||
"-crf", strconv.Itoa(opts.CRF),
|
"-crf", strconv.Itoa(opts.CRF),
|
||||||
"-threads", "4",
|
"-threads", "2",
|
||||||
)
|
)
|
||||||
if opts.UseVsync2 {
|
if opts.UseVsync2 {
|
||||||
args = append(args, "-vsync", "2")
|
args = append(args, "-vsync", "2")
|
||||||
@ -2301,6 +2301,7 @@ func generateTeaserMP4(ctx context.Context, srcPath, outPath string, startSec, d
|
|||||||
|
|
||||||
"-c:v", "libx264",
|
"-c:v", "libx264",
|
||||||
"-preset", "veryfast",
|
"-preset", "veryfast",
|
||||||
|
"-threads", "2",
|
||||||
"-crf", "28",
|
"-crf", "28",
|
||||||
"-pix_fmt", "yuv420p",
|
"-pix_fmt", "yuv420p",
|
||||||
|
|
||||||
|
|||||||
@ -92,7 +92,8 @@ func RecordStream(
|
|||||||
job.PreviewUA = hc.userAgent
|
job.PreviewUA = hc.userAgent
|
||||||
jobsMu.Unlock()
|
jobsMu.Unlock()
|
||||||
|
|
||||||
if assetID != "" {
|
// Live-Thumbs sind teuer: bei vielen parallelen Downloads nicht für alle starten.
|
||||||
|
if assetID != "" && activeRecordingJobsCount() <= 2 {
|
||||||
startLiveThumbJPGLoop(ctx, job)
|
startLiveThumbJPGLoop(ctx, job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
// backend\server.go
|
// backend\server.go
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -8,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -17,38 +17,59 @@ func main() {
|
|||||||
loadSettings()
|
loadSettings()
|
||||||
|
|
||||||
fixKeepRootFilesIntoModelSubdirs()
|
fixKeepRootFilesIntoModelSubdirs()
|
||||||
postWorkQ.StartWorkers(1)
|
|
||||||
enrichQ.StartWorkers(2)
|
postWorkQ.StartWorkers(2)
|
||||||
|
enrichQ.StartWorkers(1)
|
||||||
startPostWorkStatusRefresher()
|
startPostWorkStatusRefresher()
|
||||||
startEnrichStatusRefresher()
|
startEnrichStatusRefresher()
|
||||||
|
|
||||||
go startGeneratedGarbageCollector()
|
go startGeneratedGarbageCollector()
|
||||||
|
|
||||||
// ✅ NSFW-ONNX Detector initialisieren
|
|
||||||
if err := initNSFWDetector(); err != nil {
|
if err := initNSFWDetector(); err != nil {
|
||||||
fmt.Println("❌ NSFW-ONNX Fehler:", err)
|
fmt.Println("❌ NSFW-ONNX Fehler:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
|
var closeNSFWOnce sync.Once
|
||||||
|
closeNSFW := func() {
|
||||||
|
closeNSFWOnce.Do(func() {
|
||||||
_ = closeNSFWDetector()
|
_ = closeNSFWDetector()
|
||||||
}()
|
})
|
||||||
|
}
|
||||||
|
defer closeNSFW()
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
auth, err := NewAuthManager()
|
auth, err := NewAuthManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("❌ auth init:", err)
|
fmt.Println("❌ auth init:", err)
|
||||||
_ = closeNSFWDetector()
|
closeNSFW()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
store := registerRoutes(mux, auth)
|
store := registerRoutes(mux, auth)
|
||||||
|
setChaturbateOnlineModelStore(store)
|
||||||
|
|
||||||
|
// Hintergrund-Worker
|
||||||
go startChaturbateOnlinePoller(store)
|
go startChaturbateOnlinePoller(store)
|
||||||
go startChaturbateAutoStartWorker(store)
|
go startChaturbateAutoStartWorker(store)
|
||||||
go startMyFreeCamsAutoStartWorker(store)
|
go startMyFreeCamsAutoStartWorker(store)
|
||||||
go startDiskSpaceGuard()
|
go startDiskSpaceGuard()
|
||||||
|
|
||||||
|
// optional: Bootstrap nur im Hintergrund
|
||||||
|
if getSettings().UseChaturbateAPI {
|
||||||
|
go func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if _, err := refreshChaturbateSnapshotNow(ctx); err != nil {
|
||||||
|
fmt.Println("⚠️ [chaturbate] startup snapshot failed:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("✅ [chaturbate] startup snapshot loaded")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := ensureCoversDir(); err != nil {
|
if _, err := ensureCoversDir(); err != nil {
|
||||||
fmt.Println("⚠️ covers dir:", err)
|
fmt.Println("⚠️ covers dir:", err)
|
||||||
}
|
}
|
||||||
@ -61,7 +82,6 @@ func main() {
|
|||||||
Handler: handler,
|
Handler: handler,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown-Signale
|
|
||||||
stopSig := make(chan os.Signal, 1)
|
stopSig := make(chan os.Signal, 1)
|
||||||
signal.Notify(stopSig, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(stopSig, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
@ -73,14 +93,14 @@ func main() {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_ = srv.Shutdown(ctx)
|
_ = srv.Shutdown(ctx)
|
||||||
_ = closeNSFWDetector()
|
closeNSFW()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
fmt.Println("❌ HTTP-Server Fehler:", err)
|
fmt.Println("❌ HTTP-Server Fehler:", err)
|
||||||
_ = closeNSFWDetector()
|
closeNSFW()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = closeNSFWDetector()
|
closeNSFW()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,7 @@ type jobEvent struct {
|
|||||||
|
|
||||||
type ssePublishItem struct {
|
type ssePublishItem struct {
|
||||||
EventName string
|
EventName string
|
||||||
|
CacheKey string
|
||||||
Data []byte
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,7 +231,6 @@ func shouldPublishModelEventForJob(j *RecordJob) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func visibleJobEventsJSON() []ssePublishItem {
|
func visibleJobEventsJSON() []ssePublishItem {
|
||||||
nowTs := time.Now().UnixMilli()
|
|
||||||
out := make([]ssePublishItem, 0, 64)
|
out := make([]ssePublishItem, 0, 64)
|
||||||
|
|
||||||
jobsMu.RLock()
|
jobsMu.RLock()
|
||||||
@ -265,7 +265,7 @@ func visibleJobEventsJSON() []ssePublishItem {
|
|||||||
PreviewState: j.PreviewState,
|
PreviewState: j.PreviewState,
|
||||||
PostWorkKey: strings.TrimSpace(j.PostWorkKey),
|
PostWorkKey: strings.TrimSpace(j.PostWorkKey),
|
||||||
PostWork: j.PostWork,
|
PostWork: j.PostWork,
|
||||||
TS: nowTs,
|
TS: 0, // wichtig: stabil halten, damit Dedupe funktioniert
|
||||||
}
|
}
|
||||||
|
|
||||||
if sm := sseStoredModelForJob(j); sm != nil {
|
if sm := sseStoredModelForJob(j); sm != nil {
|
||||||
@ -287,6 +287,7 @@ func visibleJobEventsJSON() []ssePublishItem {
|
|||||||
|
|
||||||
out = append(out, ssePublishItem{
|
out = append(out, ssePublishItem{
|
||||||
EventName: eventName,
|
EventName: eventName,
|
||||||
|
CacheKey: eventName + "|" + j.ID,
|
||||||
Data: b,
|
Data: b,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -422,17 +423,42 @@ func initSSE() {
|
|||||||
|
|
||||||
// Per running job: 1 SSE event per second
|
// Per running job: 1 SSE event per second
|
||||||
go func() {
|
go func() {
|
||||||
t := time.NewTicker(1 * time.Second)
|
t := time.NewTicker(2 * time.Second)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|
||||||
|
lastByKey := map[string][]byte{}
|
||||||
|
lastHeartbeat := time.Now()
|
||||||
|
|
||||||
for range t.C {
|
for range t.C {
|
||||||
events := visibleJobEventsJSON()
|
events := visibleJobEventsJSON()
|
||||||
|
liveKeys := make(map[string]struct{}, len(events))
|
||||||
|
forceHeartbeat := time.Since(lastHeartbeat) >= 15*time.Second
|
||||||
|
|
||||||
for _, ev := range events {
|
for _, ev := range events {
|
||||||
if len(ev.Data) == 0 || ev.EventName == "" {
|
if len(ev.Data) == 0 || ev.EventName == "" || ev.CacheKey == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
liveKeys[ev.CacheKey] = struct{}{}
|
||||||
|
|
||||||
|
prev, ok := lastByKey[ev.CacheKey]
|
||||||
|
if !forceHeartbeat && ok && bytes.Equal(prev, ev.Data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lastByKey[ev.CacheKey] = append([]byte(nil), ev.Data...)
|
||||||
publishSSE(ev.EventName, ev.Data)
|
publishSSE(ev.EventName, ev.Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k := range lastByKey {
|
||||||
|
if _, ok := liveKeys[k]; !ok {
|
||||||
|
delete(lastByKey, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if forceHeartbeat {
|
||||||
|
lastHeartbeat = time.Now()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const regenerateAssetsMaxParallel = 3
|
const regenerateAssetsMaxParallel = 1
|
||||||
|
|
||||||
type regenerateAssetsRequest struct {
|
type regenerateAssetsRequest struct {
|
||||||
File string `json:"file"`
|
File string `json:"file"`
|
||||||
|
|||||||
@ -537,6 +537,9 @@ export default function App() {
|
|||||||
const lastKnownRoomStatusByKeyRef = useRef<Record<string, string>>({})
|
const lastKnownRoomStatusByKeyRef = useRef<Record<string, string>>({})
|
||||||
const roomStatusBaselineReadyRef = useRef(false)
|
const roomStatusBaselineReadyRef = useRef(false)
|
||||||
|
|
||||||
|
const pendingJobEventsRef = useRef<JobEvent[]>([])
|
||||||
|
const pendingJobEventsTimerRef = useRef<number | null>(null)
|
||||||
|
|
||||||
// ✅ Dedupe für "Cookies fehlen" Meldung (damit silent/autostarts nicht spammen)
|
// ✅ Dedupe für "Cookies fehlen" Meldung (damit silent/autostarts nicht spammen)
|
||||||
const cookieProblemLastAtRef = useRef(0)
|
const cookieProblemLastAtRef = useRef(0)
|
||||||
|
|
||||||
@ -1118,25 +1121,23 @@ export default function App() {
|
|||||||
lastSeenAtByJobIdRef.current[jobId] = now
|
lastSeenAtByJobIdRef.current[jobId] = now
|
||||||
|
|
||||||
setJobs((prev) => {
|
setJobs((prev) => {
|
||||||
const sameId = prev.filter((j) => String((j as any).id ?? '').trim() === jobId)
|
const idx = prev.findIndex(
|
||||||
const prevJob = sameId.length > 0 ? sameId[0] : null
|
(j) => String((j as any).id ?? '').trim() === jobId
|
||||||
|
)
|
||||||
|
|
||||||
|
const prevJob = idx >= 0 ? prev[idx] : null
|
||||||
const merged = mergeJob(prevJob)
|
const merged = mergeJob(prevJob)
|
||||||
|
|
||||||
const next = [
|
// bestehender Job: nur ersetzen, Reihenfolge beibehalten
|
||||||
merged,
|
if (idx >= 0) {
|
||||||
...prev.filter((j) => String((j as any).id ?? '').trim() !== jobId),
|
const next = [...prev]
|
||||||
].sort((a, b) => {
|
next[idx] = merged
|
||||||
const aMs =
|
jobsRef.current = next
|
||||||
Number((a as any)?.startedAtMs ?? 0) ||
|
return next
|
||||||
(a?.startedAt ? Date.parse(a.startedAt) || 0 : 0)
|
}
|
||||||
|
|
||||||
const bMs =
|
|
||||||
Number((b as any)?.startedAtMs ?? 0) ||
|
|
||||||
(b?.startedAt ? Date.parse(b.startedAt) || 0 : 0)
|
|
||||||
|
|
||||||
return bMs - aMs
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// neuer Job: vorne einfügen
|
||||||
|
const next = [merged, ...prev]
|
||||||
jobsRef.current = next
|
jobsRef.current = next
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
@ -1148,6 +1149,52 @@ export default function App() {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const flushQueuedJobEvents = useCallback(() => {
|
||||||
|
if (pendingJobEventsTimerRef.current != null) {
|
||||||
|
window.clearTimeout(pendingJobEventsTimerRef.current)
|
||||||
|
pendingJobEventsTimerRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const batch = pendingJobEventsRef.current
|
||||||
|
if (batch.length === 0) return
|
||||||
|
|
||||||
|
pendingJobEventsRef.current = []
|
||||||
|
|
||||||
|
// pro Job nur das letzte Event behalten
|
||||||
|
const byJobId = new Map<string, JobEvent>()
|
||||||
|
const passthrough: JobEvent[] = []
|
||||||
|
|
||||||
|
for (const msg of batch) {
|
||||||
|
const type = String(msg?.type ?? '').trim()
|
||||||
|
const jobId = String(msg?.jobId ?? '').trim()
|
||||||
|
|
||||||
|
if ((type === 'job_upsert' || type === 'job_remove') && jobId) {
|
||||||
|
byJobId.set(jobId, msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
passthrough.push(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const msg of byJobId.values()) {
|
||||||
|
applyJobDelta(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const msg of passthrough) {
|
||||||
|
applyJobDelta(msg)
|
||||||
|
}
|
||||||
|
}, [applyJobDelta])
|
||||||
|
|
||||||
|
const queueJobDelta = useCallback((msg: JobEvent) => {
|
||||||
|
pendingJobEventsRef.current.push(msg)
|
||||||
|
|
||||||
|
if (pendingJobEventsTimerRef.current != null) return
|
||||||
|
|
||||||
|
pendingJobEventsTimerRef.current = window.setTimeout(() => {
|
||||||
|
flushQueuedJobEvents()
|
||||||
|
}, 120)
|
||||||
|
}, [flushQueuedJobEvents])
|
||||||
|
|
||||||
const ensureModelEventListener = useCallback((name: string) => {
|
const ensureModelEventListener = useCallback((name: string) => {
|
||||||
const es = eventSourceRef.current
|
const es = eventSourceRef.current
|
||||||
const handler = onModelJobEventRef.current
|
const handler = onModelJobEventRef.current
|
||||||
@ -1275,6 +1322,14 @@ export default function App() {
|
|||||||
onlineLikedCount: 0,
|
onlineLikedCount: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (pendingJobEventsTimerRef.current != null) {
|
||||||
|
window.clearTimeout(pendingJobEventsTimerRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setModelsCount(headerStats.totalCount)
|
setModelsCount(headerStats.totalCount)
|
||||||
}, [headerStats.totalCount])
|
}, [headerStats.totalCount])
|
||||||
@ -1282,6 +1337,7 @@ export default function App() {
|
|||||||
const overviewInFlightRef = useRef(false)
|
const overviewInFlightRef = useRef(false)
|
||||||
const overviewLastSigRef = useRef('')
|
const overviewLastSigRef = useRef('')
|
||||||
const overviewLastAtRef = useRef(0)
|
const overviewLastAtRef = useRef(0)
|
||||||
|
const overviewTimerRef = useRef<number | null>(null)
|
||||||
|
|
||||||
const loadAppModelsSnapshot = useCallback(async (keys: string[]) => {
|
const loadAppModelsSnapshot = useCallback(async (keys: string[]) => {
|
||||||
const uniqKeys = Array.from(
|
const uniqKeys = Array.from(
|
||||||
@ -1475,12 +1531,12 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyJobDelta(msg)
|
queueJobDelta(msg)
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [applyJobDelta, applyRoomStateToModel, enqueueStart, maybeNotifyWatchedModelStatusChange])
|
}, [applyRoomStateToModel, enqueueStart, maybeNotifyWatchedModelStatusChange, queueJobDelta])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onOpen = (ev: Event) => {
|
const onOpen = (ev: Event) => {
|
||||||
@ -1782,9 +1838,19 @@ export default function App() {
|
|||||||
const lastOverviewSigRef = useRef<string>('')
|
const lastOverviewSigRef = useRef<string>('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const scheduleOverviewLoad = () => {
|
||||||
|
if (overviewTimerRef.current != null) {
|
||||||
|
window.clearTimeout(overviewTimerRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
overviewTimerRef.current = window.setTimeout(() => {
|
||||||
|
void loadAppModelsSnapshot(requiredModelKeys)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
if (lastOverviewSigRef.current !== requiredModelKeysSig) {
|
if (lastOverviewSigRef.current !== requiredModelKeysSig) {
|
||||||
lastOverviewSigRef.current = requiredModelKeysSig
|
lastOverviewSigRef.current = requiredModelKeysSig
|
||||||
void loadAppModelsSnapshot(requiredModelKeys)
|
scheduleOverviewLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChanged = (ev: Event) => {
|
const onChanged = (ev: Event) => {
|
||||||
@ -1794,6 +1860,7 @@ export default function App() {
|
|||||||
|
|
||||||
if (updated && typeof updated === 'object') {
|
if (updated && typeof updated === 'object') {
|
||||||
const k = String(updated.modelKey ?? '').toLowerCase().trim()
|
const k = String(updated.modelKey ?? '').toLowerCase().trim()
|
||||||
|
|
||||||
if (k) {
|
if (k) {
|
||||||
setModelsByKey((prev) => ({ ...prev, [k]: updated }))
|
setModelsByKey((prev) => ({ ...prev, [k]: updated }))
|
||||||
}
|
}
|
||||||
@ -1803,6 +1870,9 @@ export default function App() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
setPlayerModel((prev) => (prev?.id === updated.id ? updated : prev))
|
setPlayerModel((prev) => (prev?.id === updated.id ? updated : prev))
|
||||||
|
|
||||||
|
// counts/headerStats trotzdem später sauber nachziehen
|
||||||
|
scheduleOverviewLoad()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1821,14 +1891,23 @@ export default function App() {
|
|||||||
setPlayerModel((prev) => (prev?.id === removedId ? null : prev))
|
setPlayerModel((prev) => (prev?.id === removedId ? null : prev))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduleOverviewLoad()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadAppModelsSnapshot(requiredModelKeys)
|
scheduleOverviewLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('models-changed', onChanged as any)
|
window.addEventListener('models-changed', onChanged as any)
|
||||||
return () => window.removeEventListener('models-changed', onChanged as any)
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('models-changed', onChanged as any)
|
||||||
|
|
||||||
|
if (overviewTimerRef.current != null) {
|
||||||
|
window.clearTimeout(overviewTimerRef.current)
|
||||||
|
overviewTimerRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
}, [loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
||||||
|
|
||||||
const queuePendingAutoStart = useCallback((
|
const queuePendingAutoStart = useCallback((
|
||||||
|
|||||||
@ -578,16 +578,18 @@ export default function Downloads({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hasLiveJobs = jobs.some((j) => {
|
const activeLiveJobs = jobs.filter((j) => {
|
||||||
if ((j as any).endedAt) return false
|
if ((j as any).endedAt) return false
|
||||||
return String(j.status ?? '').trim().toLowerCase() === 'running'
|
return String(j.status ?? '').trim().toLowerCase() === 'running'
|
||||||
})
|
}).length
|
||||||
|
|
||||||
if (!hasLiveJobs || !pageVisible) return
|
if (activeLiveJobs === 0 || !pageVisible) return
|
||||||
|
|
||||||
|
const intervalMs = activeLiveJobs > 2 ? 30000 : 10000
|
||||||
|
|
||||||
const id = window.setInterval(() => {
|
const id = window.setInterval(() => {
|
||||||
setThumbTick((t) => t + 1)
|
setThumbTick((t) => t + 1)
|
||||||
}, 10000)
|
}, intervalMs)
|
||||||
|
|
||||||
return () => window.clearInterval(id)
|
return () => window.clearInterval(id)
|
||||||
}, [jobs, pageVisible])
|
}, [jobs, pageVisible])
|
||||||
@ -909,7 +911,7 @@ export default function Downloads({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasActive) return
|
if (!hasActive) return
|
||||||
const t = window.setInterval(() => setNowMs(Date.now()), 1000)
|
const t = window.setInterval(() => setNowMs(Date.now()), 5000)
|
||||||
return () => window.clearInterval(t)
|
return () => window.clearInterval(t)
|
||||||
}, [hasActive])
|
}, [hasActive])
|
||||||
|
|
||||||
@ -946,7 +948,7 @@ export default function Downloads({
|
|||||||
>
|
>
|
||||||
<ModelPreview
|
<ModelPreview
|
||||||
jobId={j.id}
|
jobId={j.id}
|
||||||
live={true}
|
live={false}
|
||||||
thumbTick={thumbTick}
|
thumbTick={thumbTick}
|
||||||
blur={blurPreviews}
|
blur={blurPreviews}
|
||||||
roomStatus={effectiveRoomStatusOfJob(
|
roomStatus={effectiveRoomStatusOfJob(
|
||||||
|
|||||||
@ -105,11 +105,6 @@ export default function ModelPreview({
|
|||||||
[jobId]
|
[jobId]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setImgSrc(previewSrc)
|
|
||||||
setImgError(false)
|
|
||||||
}, [previewSrc])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ac = new AbortController()
|
const ac = new AbortController()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user