fixed autostart
This commit is contained in:
parent
ff341755c6
commit
e5f506ec45
@ -808,10 +808,8 @@ func ensureDeferredAssetsAndAI(ctx context.Context, videoPath, sourceURL string)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := ensureTeaserForVideoCtx(ctx, videoPath, sourceURL); err != nil {
|
// NUR noch langsame Background-Schritte:
|
||||||
return fmt.Errorf("deferred teaser failed for %s: %w", videoPath, err)
|
// 1) Sprite
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := ensureSpritesForVideoCtx(ctx, videoPath, sourceURL); err != nil {
|
if _, err := ensureSpritesForVideoCtx(ctx, videoPath, sourceURL); err != nil {
|
||||||
return fmt.Errorf("deferred sprite failed for %s: %w", videoPath, err)
|
return fmt.Errorf("deferred sprite failed for %s: %w", videoPath, err)
|
||||||
}
|
}
|
||||||
@ -821,6 +819,7 @@ func ensureDeferredAssetsAndAI(ctx context.Context, videoPath, sourceURL string)
|
|||||||
return fmt.Errorf("preview-sprite.jpg fehlt nach deferred generation: %s", id)
|
return fmt.Errorf("preview-sprite.jpg fehlt nach deferred generation: %s", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2) KI-Analyse
|
||||||
analyzeReady, err := ensureAnalyzeForVideoCtx(ctx, videoPath, sourceURL, "nsfw")
|
analyzeReady, err := ensureAnalyzeForVideoCtx(ctx, videoPath, sourceURL, "nsfw")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("deferred analyze failed for %s: %w", videoPath, err)
|
return fmt.Errorf("deferred analyze failed for %s: %w", videoPath, err)
|
||||||
|
|||||||
@ -117,7 +117,7 @@ func generatePreviewSpriteJPG(
|
|||||||
"-i", videoPath,
|
"-i", videoPath,
|
||||||
"-an",
|
"-an",
|
||||||
"-sn",
|
"-sn",
|
||||||
"-threads", "0",
|
"-threads", "2",
|
||||||
"-vf", vf,
|
"-vf", vf,
|
||||||
"-frames:v", "1",
|
"-frames:v", "1",
|
||||||
"-update", "1",
|
"-update", "1",
|
||||||
|
|||||||
@ -275,6 +275,7 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showByUser := map[string]string{}
|
showByUser := map[string]string{}
|
||||||
|
imageByUser := map[string]string{}
|
||||||
seenInAPI := map[string]bool{}
|
seenInAPI := map[string]bool{}
|
||||||
|
|
||||||
for _, r := range rooms {
|
for _, r := range rooms {
|
||||||
@ -283,19 +284,26 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seenInAPI[u] = true
|
seenInAPI[u] = true
|
||||||
showByUser[u] = strings.ToLower(strings.TrimSpace(r.CurrentShow))
|
showByUser[u] = normalizePendingShowServer(r.CurrentShow)
|
||||||
|
imageByUser[u] = selectBestRoomImageURL(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// laufende Jobs sammeln
|
// laufende Jobs sammeln
|
||||||
running := map[string]bool{}
|
runningByUser := map[string]*RecordJob{}
|
||||||
jobsMu.RLock()
|
jobsMu.RLock()
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
|
if j == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !isActiveRecordingJob(j) {
|
if !isActiveRecordingJob(j) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if detectProvider(strings.TrimSpace(j.SourceURL)) != "chaturbate" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
u := chaturbateUserFromURL(j.SourceURL)
|
u := chaturbateUserFromURL(j.SourceURL)
|
||||||
if u != "" {
|
if u != "" {
|
||||||
running[u] = true
|
runningByUser[u] = j
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jobsMu.RUnlock()
|
jobsMu.RUnlock()
|
||||||
@ -348,7 +356,7 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if running[it.userKey] {
|
if runningByUser[it.userKey] != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,68 +372,86 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
queued = nextQueued
|
queued = nextQueued
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
nextPending := make([]PendingAutoStartItem, 0, len(watchedByUser))
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
// 1) watched + API sagt public => normal enqueue
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
for user, m := range watchedByUser {
|
for user, m := range watchedByUser {
|
||||||
show := strings.ToLower(strings.TrimSpace(showByUser[user]))
|
|
||||||
if show == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !strings.Contains(show, "public") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if running[user] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if queued[user] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if t, ok := lastTry[user]; ok && now.Sub(t) < retryCooldown {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
u := resolveChaturbateURL(m)
|
u := resolveChaturbateURL(m)
|
||||||
if u == "" {
|
if u == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
queue = append(queue, autoStartItem{
|
show := normalizePendingShowServer(showByUser[user])
|
||||||
userKey: user,
|
img := strings.TrimSpace(imageByUser[user])
|
||||||
url: u,
|
|
||||||
})
|
switch show {
|
||||||
queued[user] = true
|
case "public":
|
||||||
|
// public => kein Pending-Eintrag
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
queued[user] = true
|
||||||
|
|
||||||
|
case "private", "hidden", "away":
|
||||||
|
// laufenden watched-Download beenden und auf public warten
|
||||||
|
nextPending = append(nextPending, PendingAutoStartItem{
|
||||||
|
ModelKey: user,
|
||||||
|
URL: u,
|
||||||
|
Mode: "wait_public",
|
||||||
|
CurrentShow: show,
|
||||||
|
ImageURL: img,
|
||||||
|
Source: "watched",
|
||||||
|
})
|
||||||
|
|
||||||
|
if runningJob := runningByUser[user]; runningJob != nil {
|
||||||
|
stopJobsInternal([]*RecordJob{runningJob})
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// offline / unknown / nicht in API
|
||||||
|
nextProbeAtMs := now.Add(blindRetryCooldown).UnixMilli()
|
||||||
|
|
||||||
|
nextPending = append(nextPending, PendingAutoStartItem{
|
||||||
|
ModelKey: user,
|
||||||
|
URL: u,
|
||||||
|
Mode: "probe_retry",
|
||||||
|
NextProbeAtMs: nextProbeAtMs,
|
||||||
|
CurrentShow: show,
|
||||||
|
ImageURL: img,
|
||||||
|
Source: "watched",
|
||||||
|
})
|
||||||
|
|
||||||
|
if runningByUser[user] != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if queued[user] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t, ok := lastBlindTry[user]; ok && now.Sub(t) < blindRetryCooldown {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
queue = append(queue, autoStartItem{
|
||||||
|
userKey: user,
|
||||||
|
url: u,
|
||||||
|
})
|
||||||
|
queued[user] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
pendingAutoStartMu.Lock()
|
||||||
// 2) watched + NICHT in API => blind best-effort try vormerken
|
_ = saveWatchedPendingAutoStartItems(nextPending)
|
||||||
// ------------------------------------------------------------
|
pendingAutoStartMu.Unlock()
|
||||||
for user, m := range watchedByUser {
|
|
||||||
if seenInAPI[user] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if running[user] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if queued[user] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if t, ok := lastBlindTry[user]; ok && now.Sub(t) < blindRetryCooldown {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
u := resolveChaturbateURL(m)
|
|
||||||
if u == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
queue = append(queue, autoStartItem{
|
|
||||||
userKey: user,
|
|
||||||
url: u,
|
|
||||||
})
|
|
||||||
queued[user] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-startTicker.C:
|
case <-startTicker.C:
|
||||||
if isAutostartPaused() {
|
if isAutostartPaused() {
|
||||||
@ -485,6 +511,10 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pendingAutoStartMu.Lock()
|
||||||
|
_ = removeWatchedPendingAutoStartItem(it.userKey)
|
||||||
|
pendingAutoStartMu.Unlock()
|
||||||
|
|
||||||
if verboseLogs() {
|
if verboseLogs() {
|
||||||
fmt.Println("▶️ [autostart] started:", it.url)
|
fmt.Println("▶️ [autostart] started:", it.url)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -671,12 +671,13 @@ type cbOnlineOutRoom struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type cbOnlineResponse struct {
|
type cbOnlineResponse struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
FetchedAt time.Time `json:"fetchedAt"`
|
FetchedAt time.Time `json:"fetchedAt"` // letzter erfolgreicher Snapshot
|
||||||
Count int `json:"count"`
|
LastAttempt time.Time `json:"lastAttempt"` // letzter Pull-Versuch
|
||||||
Total int `json:"total"`
|
Count int `json:"count"`
|
||||||
LastError string `json:"lastError"`
|
Total int `json:"total"`
|
||||||
Rooms []cbOnlineOutRoom `json:"rooms"`
|
LastError string `json:"lastError"`
|
||||||
|
Rooms []cbOnlineOutRoom `json:"rooms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashKey(parts ...string) string {
|
func hashKey(parts ...string) string {
|
||||||
@ -696,12 +697,6 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
enabled := getSettings().UseChaturbateAPI
|
enabled := getSettings().UseChaturbateAPI
|
||||||
|
|
||||||
// UA vom Client (oder fallback)
|
|
||||||
reqUA := strings.TrimSpace(r.Header.Get("User-Agent"))
|
|
||||||
if reqUA == "" {
|
|
||||||
reqUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Request params (GET/POST)
|
// Request params (GET/POST)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@ -837,25 +832,24 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
b := (qIsHD == "1" || qIsHD == "true" || qIsHD == "yes")
|
b := (qIsHD == "1" || qIsHD == "true" || qIsHD == "yes")
|
||||||
isHD = &b
|
isHD = &b
|
||||||
}
|
}
|
||||||
|
|
||||||
allowedShow = toSet(shows)
|
allowedShow = toSet(shows)
|
||||||
}
|
}
|
||||||
|
|
||||||
onlySpecificUsers := len(users) > 0
|
onlySpecificUsers := len(users) > 0
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Response Cache (2s)
|
// Response Cache
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
cacheKey := "cb_online:" + hashKey(
|
cacheKey := "cb_online:" + hashKey(
|
||||||
fmt.Sprintf("enabled=%v", enabled),
|
fmt.Sprintf("enabled=%v", enabled),
|
||||||
"users="+strings.Join(users, ","),
|
"users="+strings.Join(users, ","),
|
||||||
"show="+strings.Join(keysOfSet(allowedShow), ","),
|
"show="+strings.Join(keysOfSet(allowedShow), ","),
|
||||||
|
|
||||||
"gender="+strings.Join(keysOfSet(allowedGender), ","),
|
"gender="+strings.Join(keysOfSet(allowedGender), ","),
|
||||||
"country="+strings.Join(keysOfSet(allowedCountry), ","),
|
"country="+strings.Join(keysOfSet(allowedCountry), ","),
|
||||||
"tagsAny="+strings.Join(keysOfSet(allowedTagsAny), ","),
|
"tagsAny="+strings.Join(keysOfSet(allowedTagsAny), ","),
|
||||||
"minUsers="+derefInt(minUsers),
|
"minUsers="+derefInt(minUsers),
|
||||||
"isHD="+derefBool(isHD),
|
"isHD="+derefBool(isHD),
|
||||||
|
|
||||||
fmt.Sprintf("refresh=%v", wantRefresh),
|
fmt.Sprintf("refresh=%v", wantRefresh),
|
||||||
"lite=1",
|
"lite=1",
|
||||||
)
|
)
|
||||||
@ -874,12 +868,13 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
|
||||||
out := cbOnlineResponse{
|
out := cbOnlineResponse{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
FetchedAt: time.Time{},
|
FetchedAt: time.Time{},
|
||||||
Count: 0,
|
LastAttempt: time.Time{},
|
||||||
Total: 0,
|
Count: 0,
|
||||||
LastError: "",
|
Total: 0,
|
||||||
Rooms: []cbOnlineOutRoom{},
|
LastError: "",
|
||||||
|
Rooms: []cbOnlineOutRoom{},
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(out)
|
body, _ := json.Marshal(out)
|
||||||
setCachedOnline(cacheKey, body)
|
setCachedOnline(cacheKey, body)
|
||||||
@ -927,7 +922,7 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
cbRefreshMu.Unlock()
|
cbRefreshMu.Unlock()
|
||||||
} else if needBootstrap && time.Since(lastAttempt) >= bootstrapCooldown {
|
} else if needBootstrap && time.Since(lastAttempt) >= bootstrapCooldown {
|
||||||
// ✅ Bootstrap darf weiterhin asynchron bleiben
|
// Bootstrap weiterhin asynchron
|
||||||
cbRefreshMu.Lock()
|
cbRefreshMu.Lock()
|
||||||
if cbRefreshInFlight {
|
if cbRefreshInFlight {
|
||||||
cbRefreshMu.Unlock()
|
cbRefreshMu.Unlock()
|
||||||
@ -954,7 +949,7 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ JETZT erst den finalen Snapshot lesen
|
// finalen Snapshot lesen
|
||||||
cbMu.RLock()
|
cbMu.RLock()
|
||||||
fetchedAt = cb.FetchedAt
|
fetchedAt = cb.FetchedAt
|
||||||
lastErr = cb.LastErr
|
lastErr = cb.LastErr
|
||||||
@ -976,9 +971,8 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Rooms bauen (LITE, O(Anzahl requested Users))
|
// Rooms bauen (LITE)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
matches := func(rm ChaturbateOnlineRoomLite) bool {
|
matches := func(rm ChaturbateOnlineRoomLite) bool {
|
||||||
if len(allowedShow) > 0 {
|
if len(allowedShow) > 0 {
|
||||||
s := strings.ToLower(strings.TrimSpace(rm.CurrentShow))
|
s := strings.ToLower(strings.TrimSpace(rm.CurrentShow))
|
||||||
@ -1065,12 +1059,13 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
|
||||||
out := cbOnlineResponse{
|
out := cbOnlineResponse{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
FetchedAt: fetchedAt,
|
FetchedAt: fetchedAt,
|
||||||
Count: len(outRooms),
|
LastAttempt: lastAttempt,
|
||||||
Total: total,
|
Count: len(outRooms),
|
||||||
LastError: lastErr,
|
Total: total,
|
||||||
Rooms: outRooms,
|
LastError: lastErr,
|
||||||
|
Rooms: outRooms,
|
||||||
}
|
}
|
||||||
|
|
||||||
body, _ := json.Marshal(out)
|
body, _ := json.Marshal(out)
|
||||||
|
|||||||
@ -21,6 +21,7 @@ type PendingAutoStartItem struct {
|
|||||||
NextProbeAtMs int64 `json:"nextProbeAtMs,omitempty"` // nur bei probe_retry
|
NextProbeAtMs int64 `json:"nextProbeAtMs,omitempty"` // nur bei probe_retry
|
||||||
CurrentShow string `json:"currentShow,omitempty"` // public/private/hidden/away/offline/unknown
|
CurrentShow string `json:"currentShow,omitempty"` // public/private/hidden/away/offline/unknown
|
||||||
ImageURL string `json:"imageUrl,omitempty"`
|
ImageURL string `json:"imageUrl,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"` // manual | watched
|
||||||
}
|
}
|
||||||
|
|
||||||
type PendingAutoStartResponse struct {
|
type PendingAutoStartResponse struct {
|
||||||
@ -33,6 +34,17 @@ type pendingAutoStartFile struct {
|
|||||||
|
|
||||||
var pendingAutoStartMu sync.Mutex
|
var pendingAutoStartMu sync.Mutex
|
||||||
|
|
||||||
|
const pendingAutoStartGlobalUserKey = "__global__"
|
||||||
|
|
||||||
|
func normalizePendingSourceServer(v string) string {
|
||||||
|
switch strings.TrimSpace(strings.ToLower(v)) {
|
||||||
|
case "watched":
|
||||||
|
return "watched"
|
||||||
|
default:
|
||||||
|
return "manual"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func normalizePendingModeServer(v string) string {
|
func normalizePendingModeServer(v string) string {
|
||||||
if strings.TrimSpace(strings.ToLower(v)) == "probe_retry" {
|
if strings.TrimSpace(strings.ToLower(v)) == "probe_retry" {
|
||||||
return "probe_retry"
|
return "probe_retry"
|
||||||
@ -103,12 +115,94 @@ func loadPendingAutoStartItems(userKey string) ([]PendingAutoStartItem, error) {
|
|||||||
NextProbeAtMs: it.NextProbeAtMs,
|
NextProbeAtMs: it.NextProbeAtMs,
|
||||||
CurrentShow: normalizePendingShowServer(it.CurrentShow),
|
CurrentShow: normalizePendingShowServer(it.CurrentShow),
|
||||||
ImageURL: strings.TrimSpace(it.ImageURL),
|
ImageURL: strings.TrimSpace(it.ImageURL),
|
||||||
|
Source: normalizePendingSourceServer(it.Source),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadMergedPendingAutoStartItems(userKey string) ([]PendingAutoStartItem, error) {
|
||||||
|
globalItems, err := loadPendingAutoStartItems(pendingAutoStartGlobalUserKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userItems, err := loadPendingAutoStartItems(userKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
byKey := make(map[string]PendingAutoStartItem)
|
||||||
|
|
||||||
|
for _, it := range globalItems {
|
||||||
|
k := strings.ToLower(strings.TrimSpace(it.ModelKey))
|
||||||
|
if k == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byKey[k] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// user-spezifisch überschreibt global
|
||||||
|
for _, it := range userItems {
|
||||||
|
k := strings.ToLower(strings.TrimSpace(it.ModelKey))
|
||||||
|
if k == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byKey[k] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]PendingAutoStartItem, 0, len(byKey))
|
||||||
|
for _, it := range byKey {
|
||||||
|
out = append(out, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveWatchedPendingAutoStartItems(items []PendingAutoStartItem) error {
|
||||||
|
next := make([]PendingAutoStartItem, 0, len(items))
|
||||||
|
|
||||||
|
for _, it := range items {
|
||||||
|
it.ModelKey = strings.ToLower(strings.TrimSpace(it.ModelKey))
|
||||||
|
it.URL = strings.TrimSpace(it.URL)
|
||||||
|
it.Mode = normalizePendingModeServer(it.Mode)
|
||||||
|
it.CurrentShow = normalizePendingShowServer(it.CurrentShow)
|
||||||
|
it.ImageURL = strings.TrimSpace(it.ImageURL)
|
||||||
|
it.Source = "watched"
|
||||||
|
|
||||||
|
if it.ModelKey == "" || it.URL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
next = append(next, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return savePendingAutoStartItems(pendingAutoStartGlobalUserKey, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeWatchedPendingAutoStartItem(modelKey string) error {
|
||||||
|
modelKey = strings.ToLower(strings.TrimSpace(modelKey))
|
||||||
|
if modelKey == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := loadPendingAutoStartItems(pendingAutoStartGlobalUserKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
next := make([]PendingAutoStartItem, 0, len(items))
|
||||||
|
for _, it := range items {
|
||||||
|
if strings.ToLower(strings.TrimSpace(it.ModelKey)) == modelKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next = append(next, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return savePendingAutoStartItems(pendingAutoStartGlobalUserKey, next)
|
||||||
|
}
|
||||||
|
|
||||||
func savePendingAutoStartItems(userKey string, items []PendingAutoStartItem) error {
|
func savePendingAutoStartItems(userKey string, items []PendingAutoStartItem) error {
|
||||||
path := pendingAutoStartFilePath(userKey)
|
path := pendingAutoStartFilePath(userKey)
|
||||||
|
|
||||||
@ -148,7 +242,7 @@ func handlePendingAutoStart(auth *AuthManager) http.HandlerFunc {
|
|||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
items, err := loadPendingAutoStartItems(userKey)
|
items, err := loadMergedPendingAutoStartItems(userKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -172,6 +266,7 @@ func handlePendingAutoStart(auth *AuthManager) http.HandlerFunc {
|
|||||||
in.Mode = normalizePendingModeServer(in.Mode)
|
in.Mode = normalizePendingModeServer(in.Mode)
|
||||||
in.CurrentShow = normalizePendingShowServer(in.CurrentShow)
|
in.CurrentShow = normalizePendingShowServer(in.CurrentShow)
|
||||||
in.ImageURL = strings.TrimSpace(in.ImageURL)
|
in.ImageURL = strings.TrimSpace(in.ImageURL)
|
||||||
|
in.Source = normalizePendingSourceServer(in.Source)
|
||||||
|
|
||||||
if in.ModelKey == "" {
|
if in.ModelKey == "" {
|
||||||
http.Error(w, "missing modelKey", http.StatusBadRequest)
|
http.Error(w, "missing modelKey", http.StatusBadRequest)
|
||||||
|
|||||||
@ -2310,6 +2310,7 @@ func generateTeaserMP4(ctx context.Context, srcPath, outPath string, startSec, d
|
|||||||
|
|
||||||
"-movflags", "+faststart",
|
"-movflags", "+faststart",
|
||||||
"-f", "mp4",
|
"-f", "mp4",
|
||||||
|
"-threads", "2",
|
||||||
tmp,
|
tmp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1473,6 +1473,7 @@ func recordStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
jobsMu.RUnlock()
|
jobsMu.RUnlock()
|
||||||
|
|
||||||
applyPreviewSpriteTruthToRecordJobMeta(&c)
|
applyPreviewSpriteTruthToRecordJobMeta(&c)
|
||||||
|
applyFinishedPhaseTruthToRecordJobMeta(&c)
|
||||||
respondJSON(w, &c)
|
respondJSON(w, &c)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2300,7 +2301,13 @@ func recordDoneList(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c := *base
|
c := *base
|
||||||
|
|
||||||
|
// Sprite-Infos aus Dateisystem/meta ergänzen
|
||||||
applyPreviewSpriteTruthToRecordJobMeta(&c)
|
applyPreviewSpriteTruthToRecordJobMeta(&c)
|
||||||
|
|
||||||
|
// WICHTIG: completed-Flags für Meta/Thumb/Teaser/Sprites/Analyze setzen
|
||||||
|
applyFinishedPhaseTruthToRecordJobMeta(&c)
|
||||||
|
|
||||||
out = append(out, &c)
|
out = append(out, &c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1184
backend/recorder.go
1184
backend/recorder.go
File diff suppressed because it is too large
Load Diff
@ -257,50 +257,37 @@ func runGenerateMissingAssets(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialisierung: Total etc. + SSE Push
|
// Initialisierung: Total etc. + SSE Push
|
||||||
updateAssetsState(func(st *AssetsTaskState) {
|
type assetCandidate struct {
|
||||||
st.Total = len(items)
|
name string
|
||||||
st.Done = 0
|
path string
|
||||||
st.GeneratedThumbs = 0
|
id string
|
||||||
st.GeneratedPreviews = 0
|
beforeTruth finishedPhaseTruth
|
||||||
st.Skipped = 0
|
}
|
||||||
st.Error = ""
|
|
||||||
})
|
|
||||||
|
|
||||||
for i, it := range items {
|
candidates := make([]assetCandidate, 0, len(items))
|
||||||
|
skippedCount := 0
|
||||||
|
|
||||||
|
for _, it := range items {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
finishWithErr(err)
|
finishWithErr(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// aktuellen Dateinamen für UI setzen
|
|
||||||
updateAssetsState(func(st *AssetsTaskState) {
|
|
||||||
st.CurrentFile = it.name
|
|
||||||
})
|
|
||||||
|
|
||||||
// ID aus Dateiname
|
|
||||||
base := strings.TrimSuffix(it.name, filepath.Ext(it.name))
|
base := strings.TrimSuffix(it.name, filepath.Ext(it.name))
|
||||||
id := stripHotPrefix(base)
|
id := stripHotPrefix(base)
|
||||||
if strings.TrimSpace(id) == "" {
|
if strings.TrimSpace(id) == "" {
|
||||||
clearAssetsTaskFinishedPostworkStates(it.name)
|
clearAssetsTaskFinishedPostworkStates(it.name)
|
||||||
|
skippedCount++
|
||||||
updateAssetsState(func(st *AssetsTaskState) {
|
|
||||||
st.Done = i + 1
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Datei-Info (validieren)
|
|
||||||
vfi, verr := os.Stat(it.path)
|
vfi, verr := os.Stat(it.path)
|
||||||
if verr != nil || vfi.IsDir() || vfi.Size() <= 0 {
|
if verr != nil || vfi == nil || vfi.IsDir() || vfi.Size() <= 0 {
|
||||||
clearAssetsTaskFinishedPostworkStates(it.name)
|
clearAssetsTaskFinishedPostworkStates(it.name)
|
||||||
|
skippedCount++
|
||||||
updateAssetsState(func(st *AssetsTaskState) {
|
|
||||||
st.Done = i + 1
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vorher-Wahrheit ermitteln: was fehlt wirklich?
|
|
||||||
beforeTruth := finishedPhaseTruthForID(id)
|
beforeTruth := finishedPhaseTruthForID(id)
|
||||||
|
|
||||||
needMeta := !beforeTruth.MetaReady
|
needMeta := !beforeTruth.MetaReady
|
||||||
@ -309,17 +296,69 @@ func runGenerateMissingAssets(ctx context.Context) {
|
|||||||
needSprites := !beforeTruth.SpritesReady
|
needSprites := !beforeTruth.SpritesReady
|
||||||
needAnalyze := !beforeTruth.AnalyzeReady
|
needAnalyze := !beforeTruth.AnalyzeReady
|
||||||
|
|
||||||
// Wenn gar nichts fehlt -> alte evtl. Live-States wegräumen und skippen
|
// Bereits vollständig -> gar nicht erst in die Arbeitsliste aufnehmen
|
||||||
if !needMeta && !needThumb && !needTeaser && !needSprites && !needAnalyze {
|
if !needMeta && !needThumb && !needTeaser && !needSprites && !needAnalyze {
|
||||||
clearAssetsTaskFinishedPostworkStates(it.name)
|
clearAssetsTaskFinishedPostworkStates(it.name)
|
||||||
|
skippedCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates = append(candidates, assetCandidate{
|
||||||
|
name: it.name,
|
||||||
|
path: it.path,
|
||||||
|
id: id,
|
||||||
|
beforeTruth: beforeTruth,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAssetsState(func(st *AssetsTaskState) {
|
||||||
|
st.Total = len(candidates)
|
||||||
|
st.Done = 0
|
||||||
|
st.GeneratedThumbs = 0
|
||||||
|
st.GeneratedPreviews = 0
|
||||||
|
st.Skipped = skippedCount
|
||||||
|
st.Error = ""
|
||||||
|
st.CurrentFile = ""
|
||||||
|
st.CurrentQueue = ""
|
||||||
|
st.CurrentPhase = ""
|
||||||
|
st.CurrentLabel = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
finishWithErr(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, it := range candidates {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
finishWithErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAssetsState(func(st *AssetsTaskState) {
|
||||||
|
st.CurrentFile = it.name
|
||||||
|
})
|
||||||
|
|
||||||
|
id := it.id
|
||||||
|
|
||||||
|
vfi, verr := os.Stat(it.path)
|
||||||
|
if verr != nil || vfi == nil || vfi.IsDir() || vfi.Size() <= 0 {
|
||||||
|
clearAssetsTaskFinishedPostworkStates(it.name)
|
||||||
|
|
||||||
updateAssetsState(func(st *AssetsTaskState) {
|
updateAssetsState(func(st *AssetsTaskState) {
|
||||||
st.Skipped++
|
|
||||||
st.Done = i + 1
|
st.Done = i + 1
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeTruth := it.beforeTruth
|
||||||
|
|
||||||
|
needMeta := !beforeTruth.MetaReady
|
||||||
|
needThumb := !beforeTruth.ThumbReady
|
||||||
|
needTeaser := !beforeTruth.TeaserReady
|
||||||
|
needSprites := !beforeTruth.SpritesReady
|
||||||
|
needAnalyze := !beforeTruth.AnalyzeReady
|
||||||
|
|
||||||
// Pfade einmalig über zentralen Helper
|
// Pfade einmalig über zentralen Helper
|
||||||
_, _, _, _, metaPath, perr := assetPathsForID(id)
|
_, _, _, _, metaPath, perr := assetPathsForID(id)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
|
|||||||
@ -190,6 +190,7 @@ type ChaturbateOnlineRoomItem = {
|
|||||||
type ChaturbateOnlineResponse = {
|
type ChaturbateOnlineResponse = {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
fetchedAt?: string
|
fetchedAt?: string
|
||||||
|
lastAttempt?: string
|
||||||
count?: number
|
count?: number
|
||||||
total?: number
|
total?: number
|
||||||
lastError?: string
|
lastError?: string
|
||||||
@ -204,6 +205,8 @@ type AutostartState = {
|
|||||||
|
|
||||||
type PendingAutoStartMode = 'wait_public' | 'probe_retry'
|
type PendingAutoStartMode = 'wait_public' | 'probe_retry'
|
||||||
|
|
||||||
|
type PendingAutoStartSource = 'manual' | 'watched'
|
||||||
|
|
||||||
type PendingAutoStartItem = {
|
type PendingAutoStartItem = {
|
||||||
modelKey: string
|
modelKey: string
|
||||||
url: string
|
url: string
|
||||||
@ -211,6 +214,7 @@ type PendingAutoStartItem = {
|
|||||||
nextProbeAtMs?: number
|
nextProbeAtMs?: number
|
||||||
currentShow?: 'public' | 'private' | 'hidden' | 'away' | 'offline' | 'unknown'
|
currentShow?: 'public' | 'private' | 'hidden' | 'away' | 'offline' | 'unknown'
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
|
source?: PendingAutoStartSource
|
||||||
}
|
}
|
||||||
|
|
||||||
type PendingAutoStartResponse = {
|
type PendingAutoStartResponse = {
|
||||||
@ -393,6 +397,11 @@ function stripHotPrefix(name: string) {
|
|||||||
const reModel = /^(.*?)_\d{1,2}_\d{1,2}_\d{4}__\d{1,2}-\d{2}-\d{2}/
|
const reModel = /^(.*?)_\d{1,2}_\d{1,2}_\d{4}__\d{1,2}-\d{2}-\d{2}/
|
||||||
|
|
||||||
function chaturbateModelKeyFromJob(job: RecordJob): string {
|
function chaturbateModelKeyFromJob(job: RecordJob): string {
|
||||||
|
const direct = String((job as any)?.model ?? (job as any)?.modelKey ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
if (direct) return direct
|
||||||
|
|
||||||
const raw = String((job as any)?.sourceUrl ?? '').trim()
|
const raw = String((job as any)?.sourceUrl ?? '').trim()
|
||||||
const norm0 = normalizeHttpUrl(raw)
|
const norm0 = normalizeHttpUrl(raw)
|
||||||
const norm = norm0 ? canonicalizeProviderUrl(norm0) : ''
|
const norm = norm0 ? canonicalizeProviderUrl(norm0) : ''
|
||||||
@ -400,7 +409,9 @@ function chaturbateModelKeyFromJob(job: RecordJob): string {
|
|||||||
const keyFromUrl = providerKeyLowerFromUrl(norm)
|
const keyFromUrl = providerKeyLowerFromUrl(norm)
|
||||||
if (keyFromUrl) return keyFromUrl
|
if (keyFromUrl) return keyFromUrl
|
||||||
|
|
||||||
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '').trim().toLowerCase()
|
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
return keyFromFile
|
return keyFromFile
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,6 +429,11 @@ function modelKeyFromFilename(fileOrPath: string): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function modelEventKeyFromJob(job: RecordJob): string {
|
function modelEventKeyFromJob(job: RecordJob): string {
|
||||||
|
const direct = String((job as any)?.model ?? (job as any)?.modelKey ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
if (direct) return direct
|
||||||
|
|
||||||
const raw = String((job as any)?.sourceUrl ?? '').trim()
|
const raw = String((job as any)?.sourceUrl ?? '').trim()
|
||||||
const norm0 = normalizeHttpUrl(raw)
|
const norm0 = normalizeHttpUrl(raw)
|
||||||
const norm = norm0 ? canonicalizeProviderUrl(norm0) : ''
|
const norm = norm0 ? canonicalizeProviderUrl(norm0) : ''
|
||||||
@ -425,7 +441,9 @@ function modelEventKeyFromJob(job: RecordJob): string {
|
|||||||
const keyFromUrl = providerKeyLowerFromUrl(norm)
|
const keyFromUrl = providerKeyLowerFromUrl(norm)
|
||||||
if (keyFromUrl) return keyFromUrl
|
if (keyFromUrl) return keyFromUrl
|
||||||
|
|
||||||
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '').trim().toLowerCase()
|
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
return keyFromFile
|
return keyFromFile
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,7 +453,9 @@ function modelEventKeyFromDoneJob(job: RecordJob): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function modelKeyAndHostFromJob(job: RecordJob): { modelKey: string; host: string } {
|
function modelKeyAndHostFromJob(job: RecordJob): { modelKey: string; host: string } {
|
||||||
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '').trim().toLowerCase()
|
const direct = String((job as any)?.model ?? (job as any)?.modelKey ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
let host = ''
|
let host = ''
|
||||||
try {
|
try {
|
||||||
@ -449,6 +469,14 @@ function modelKeyAndHostFromJob(job: RecordJob): { modelKey: string; host: strin
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (direct) {
|
||||||
|
return { modelKey: direct, host }
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
if (keyFromFile) {
|
if (keyFromFile) {
|
||||||
return { modelKey: keyFromFile, host }
|
return { modelKey: keyFromFile, host }
|
||||||
}
|
}
|
||||||
@ -583,6 +611,13 @@ export default function App() {
|
|||||||
const [pendingAutoStartModeByKey, setPendingAutoStartModeByKey] = useState<Record<string, PendingAutoStartMode>>({})
|
const [pendingAutoStartModeByKey, setPendingAutoStartModeByKey] = useState<Record<string, PendingAutoStartMode>>({})
|
||||||
const [pendingBlindRetryAtByKey, setPendingBlindRetryAtByKey] = useState<Record<string, number>>({})
|
const [pendingBlindRetryAtByKey, setPendingBlindRetryAtByKey] = useState<Record<string, number>>({})
|
||||||
|
|
||||||
|
const [pendingAutoStartSourceByKey, setPendingAutoStartSourceByKey] = useState<Record<string, PendingAutoStartSource>>({})
|
||||||
|
|
||||||
|
const pendingAutoStartSourceByKeyRef = useRef(pendingAutoStartSourceByKey)
|
||||||
|
useEffect(() => {
|
||||||
|
pendingAutoStartSourceByKeyRef.current = pendingAutoStartSourceByKey
|
||||||
|
}, [pendingAutoStartSourceByKey])
|
||||||
|
|
||||||
const pendingAutoStartModeByKeyRef = useRef(pendingAutoStartModeByKey)
|
const pendingAutoStartModeByKeyRef = useRef(pendingAutoStartModeByKey)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pendingAutoStartModeByKeyRef.current = pendingAutoStartModeByKey
|
pendingAutoStartModeByKeyRef.current = pendingAutoStartModeByKey
|
||||||
@ -687,10 +722,12 @@ export default function App() {
|
|||||||
setPendingAutoStartByKey({})
|
setPendingAutoStartByKey({})
|
||||||
setPendingAutoStartModeByKey({})
|
setPendingAutoStartModeByKey({})
|
||||||
setPendingBlindRetryAtByKey({})
|
setPendingBlindRetryAtByKey({})
|
||||||
|
setPendingAutoStartSourceByKey({})
|
||||||
|
|
||||||
pendingAutoStartByKeyRef.current = {}
|
pendingAutoStartByKeyRef.current = {}
|
||||||
pendingAutoStartModeByKeyRef.current = {}
|
pendingAutoStartModeByKeyRef.current = {}
|
||||||
pendingBlindRetryAtByKeyRef.current = {}
|
pendingBlindRetryAtByKeyRef.current = {}
|
||||||
|
pendingAutoStartSourceByKeyRef.current = {}
|
||||||
|
|
||||||
// optional: URL-Feld leeren
|
// optional: URL-Feld leeren
|
||||||
setSourceUrl('')
|
setSourceUrl('')
|
||||||
@ -924,12 +961,15 @@ export default function App() {
|
|||||||
const nextByKey: Record<string, string> = {}
|
const nextByKey: Record<string, string> = {}
|
||||||
const nextModeByKey: Record<string, PendingAutoStartMode> = {}
|
const nextModeByKey: Record<string, PendingAutoStartMode> = {}
|
||||||
const nextBlindRetryAtByKey: Record<string, number> = {}
|
const nextBlindRetryAtByKey: Record<string, number> = {}
|
||||||
|
const nextSourceByKey: Record<string, PendingAutoStartSource> = {}
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const keyLower = String(item?.modelKey ?? '').trim().toLowerCase()
|
const keyLower = String(item?.modelKey ?? '').trim().toLowerCase()
|
||||||
const norm0 = normalizeHttpUrl(String(item?.url ?? ''))
|
const norm0 = normalizeHttpUrl(String(item?.url ?? ''))
|
||||||
const mode: PendingAutoStartMode =
|
const mode: PendingAutoStartMode =
|
||||||
String(item?.mode ?? '').trim() === 'probe_retry' ? 'probe_retry' : 'wait_public'
|
String(item?.mode ?? '').trim() === 'probe_retry' ? 'probe_retry' : 'wait_public'
|
||||||
|
const source: PendingAutoStartSource =
|
||||||
|
String(item?.source ?? '').trim() === 'watched' ? 'watched' : 'manual'
|
||||||
|
|
||||||
if (!keyLower || !norm0) continue
|
if (!keyLower || !norm0) continue
|
||||||
|
|
||||||
@ -937,6 +977,7 @@ export default function App() {
|
|||||||
|
|
||||||
nextByKey[keyLower] = norm
|
nextByKey[keyLower] = norm
|
||||||
nextModeByKey[keyLower] = mode
|
nextModeByKey[keyLower] = mode
|
||||||
|
nextSourceByKey[keyLower] = source
|
||||||
|
|
||||||
if (mode === 'probe_retry') {
|
if (mode === 'probe_retry') {
|
||||||
const ts = Number(item?.nextProbeAtMs ?? 0)
|
const ts = Number(item?.nextProbeAtMs ?? 0)
|
||||||
@ -953,6 +994,9 @@ export default function App() {
|
|||||||
|
|
||||||
setPendingBlindRetryAtByKey(nextBlindRetryAtByKey)
|
setPendingBlindRetryAtByKey(nextBlindRetryAtByKey)
|
||||||
pendingBlindRetryAtByKeyRef.current = nextBlindRetryAtByKey
|
pendingBlindRetryAtByKeyRef.current = nextBlindRetryAtByKey
|
||||||
|
|
||||||
|
setPendingAutoStartSourceByKey(nextSourceByKey)
|
||||||
|
pendingAutoStartSourceByKeyRef.current = nextSourceByKey
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
@ -1072,6 +1116,8 @@ export default function App() {
|
|||||||
return {
|
return {
|
||||||
...(prevJob ?? ({} as RecordJob)),
|
...(prevJob ?? ({} as RecordJob)),
|
||||||
id: jobId,
|
id: jobId,
|
||||||
|
model: msg.model ?? (prevJob as any)?.model,
|
||||||
|
|
||||||
status: (msg.status as any) ?? (prevJob?.status ?? 'running'),
|
status: (msg.status as any) ?? (prevJob?.status ?? 'running'),
|
||||||
sourceUrl: msg.sourceUrl ?? ((prevJob as any)?.sourceUrl ?? ''),
|
sourceUrl: msg.sourceUrl ?? ((prevJob as any)?.sourceUrl ?? ''),
|
||||||
output: msg.output ?? (prevJob?.output ?? ''),
|
output: msg.output ?? (prevJob?.output ?? ''),
|
||||||
@ -1681,6 +1727,7 @@ export default function App() {
|
|||||||
const alreadyRunning = jobsRef.current.some((j) => {
|
const alreadyRunning = jobsRef.current.some((j) => {
|
||||||
if (String(j.status || '').toLowerCase() !== 'running') return false
|
if (String(j.status || '').toLowerCase() !== 'running') return false
|
||||||
if ((j as any).endedAt) return false
|
if ((j as any).endedAt) return false
|
||||||
|
|
||||||
const jNorm0 = normalizeHttpUrl(String((j as any).sourceUrl || ''))
|
const jNorm0 = normalizeHttpUrl(String((j as any).sourceUrl || ''))
|
||||||
const jNorm = jNorm0 ? canonicalizeProviderUrl(jNorm0) : ''
|
const jNorm = jNorm0 ? canonicalizeProviderUrl(jNorm0) : ''
|
||||||
return jNorm === normUrl
|
return jNorm === normUrl
|
||||||
@ -1697,7 +1744,9 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (provider === 'chaturbate' && !hasRequiredChaturbateCookies(currentCookies)) {
|
if (provider === 'chaturbate' && !hasRequiredChaturbateCookies(currentCookies)) {
|
||||||
if (!silent) setError('Für Chaturbate müssen die Cookies "cf_clearance" und "sessionId" gesetzt sein.')
|
if (!silent) {
|
||||||
|
setError('Für Chaturbate müssen die Cookies "cf_clearance" und "sessionId" gesetzt sein.')
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1715,13 +1764,20 @@ export default function App() {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (created?.id && !opts?.hidden) {
|
// Wichtig: model lokal sofort mitschleppen, damit requiredModelKeys /
|
||||||
startedToastByJobIdRef.current[String(created.id)] = true
|
// overview / watched/fav/liked schon vor SSE korrekt funktionieren.
|
||||||
|
const localModelKey = providerKeyLowerFromUrl(normUrl)
|
||||||
|
const createdWithModel = localModelKey
|
||||||
|
? ({ ...created, model: localModelKey } as RecordJob)
|
||||||
|
: created
|
||||||
|
|
||||||
|
if ((createdWithModel as any)?.id && !opts?.hidden) {
|
||||||
|
startedToastByJobIdRef.current[String((createdWithModel as any).id)] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(opts?.hidden || Boolean((created as any)?.hidden))) {
|
if (!(opts?.hidden || Boolean((createdWithModel as any)?.hidden))) {
|
||||||
setJobs((prev) => {
|
setJobs((prev) => {
|
||||||
const next = upsertJobById(prev, created)
|
const next = upsertJobById(prev, createdWithModel)
|
||||||
jobsRef.current = next
|
jobsRef.current = next
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
@ -1833,11 +1889,17 @@ export default function App() {
|
|||||||
[requiredModelKeys]
|
[requiredModelKeys]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const requiredModelKeysRef = useRef<string[]>(requiredModelKeys)
|
||||||
|
|
||||||
const lastOverviewSigRef = useRef<string>('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
requiredModelKeysRef.current = requiredModelKeys
|
||||||
|
}, [requiredModelKeys])
|
||||||
|
|
||||||
|
const lastOverviewSigRef = useRef<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authed) return
|
||||||
|
|
||||||
const scheduleOverviewLoad = () => {
|
const scheduleOverviewLoad = () => {
|
||||||
if (overviewTimerRef.current != null) {
|
if (overviewTimerRef.current != null) {
|
||||||
window.clearTimeout(overviewTimerRef.current)
|
window.clearTimeout(overviewTimerRef.current)
|
||||||
@ -1870,8 +1932,6 @@ 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()
|
scheduleOverviewLoad()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1908,7 +1968,7 @@ export default function App() {
|
|||||||
overviewTimerRef.current = null
|
overviewTimerRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
}, [authed, loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
||||||
|
|
||||||
const queuePendingAutoStart = useCallback((
|
const queuePendingAutoStart = useCallback((
|
||||||
keyLower: string,
|
keyLower: string,
|
||||||
@ -1918,6 +1978,7 @@ export default function App() {
|
|||||||
opts?: {
|
opts?: {
|
||||||
mode?: PendingAutoStartMode
|
mode?: PendingAutoStartMode
|
||||||
nextProbeAtMs?: number
|
nextProbeAtMs?: number
|
||||||
|
source?: PendingAutoStartSource
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const key = String(keyLower ?? '').trim().toLowerCase()
|
const key = String(keyLower ?? '').trim().toLowerCase()
|
||||||
@ -1926,11 +1987,31 @@ export default function App() {
|
|||||||
|
|
||||||
const norm = canonicalizeProviderUrl(norm0)
|
const norm = canonicalizeProviderUrl(norm0)
|
||||||
const mode: PendingAutoStartMode = opts?.mode ?? 'wait_public'
|
const mode: PendingAutoStartMode = opts?.mode ?? 'wait_public'
|
||||||
|
const source: PendingAutoStartSource = opts?.source === 'watched' ? 'watched' : 'manual'
|
||||||
|
|
||||||
const nextProbeAtMs =
|
const nextProbeAtMs =
|
||||||
mode === 'probe_retry'
|
mode === 'probe_retry'
|
||||||
? Number(opts?.nextProbeAtMs ?? (Date.now() + 60_000))
|
? Number(opts?.nextProbeAtMs ?? (Date.now() + 60_000))
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const nextShow = normalizePendingShow(show)
|
||||||
|
const nextRetryAt =
|
||||||
|
mode === 'probe_retry' && Number.isFinite(nextProbeAtMs)
|
||||||
|
? Number(nextProbeAtMs)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
// ✅ ALTE Werte VOR dem State-Update lesen
|
||||||
|
const prevUrl = String(pendingAutoStartByKeyRef.current[key] ?? '').trim()
|
||||||
|
const prevMode = (pendingAutoStartModeByKeyRef.current[key] ?? 'wait_public') as PendingAutoStartMode
|
||||||
|
const prevSource = (pendingAutoStartSourceByKeyRef.current[key] ?? 'manual') as PendingAutoStartSource
|
||||||
|
const prevRetryAt = Number(pendingBlindRetryAtByKeyRef.current[key] ?? 0)
|
||||||
|
|
||||||
|
const sameUrl = prevUrl === norm
|
||||||
|
const sameMode = prevMode === mode
|
||||||
|
const sameSource = prevSource === source
|
||||||
|
const sameRetryAt =
|
||||||
|
mode !== 'probe_retry' || Math.abs(prevRetryAt - nextRetryAt) < 1000
|
||||||
|
|
||||||
setPendingAutoStartByKey((prev) => {
|
setPendingAutoStartByKey((prev) => {
|
||||||
const next = { ...(prev || {}), [key]: norm }
|
const next = { ...(prev || {}), [key]: norm }
|
||||||
pendingAutoStartByKeyRef.current = next
|
pendingAutoStartByKeyRef.current = next
|
||||||
@ -1943,11 +2024,17 @@ export default function App() {
|
|||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setPendingAutoStartSourceByKey((prev) => {
|
||||||
|
const next = { ...(prev || {}), [key]: source }
|
||||||
|
pendingAutoStartSourceByKeyRef.current = next
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
setPendingBlindRetryAtByKey((prev) => {
|
setPendingBlindRetryAtByKey((prev) => {
|
||||||
const next = { ...(prev || {}) }
|
const next = { ...(prev || {}) }
|
||||||
|
|
||||||
if (mode === 'probe_retry') {
|
if (mode === 'probe_retry') {
|
||||||
next[key] = Number.isFinite(nextProbeAtMs) ? Number(nextProbeAtMs) : Date.now() + 60_000
|
next[key] = nextRetryAt || (Date.now() + 60_000)
|
||||||
} else {
|
} else {
|
||||||
delete next[key]
|
delete next[key]
|
||||||
}
|
}
|
||||||
@ -1961,7 +2048,7 @@ export default function App() {
|
|||||||
id: key,
|
id: key,
|
||||||
modelKey: key,
|
modelKey: key,
|
||||||
url: norm,
|
url: norm,
|
||||||
currentShow: normalizePendingShow(show),
|
currentShow: nextShow,
|
||||||
imageUrl: imageUrl || undefined,
|
imageUrl: imageUrl || undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1978,20 +2065,8 @@ export default function App() {
|
|||||||
return [nextItem, ...prev]
|
return [nextItem, ...prev]
|
||||||
})
|
})
|
||||||
|
|
||||||
const prevUrl = String(pendingAutoStartByKeyRef.current[key] ?? '').trim()
|
// ✅ Nur persistieren, wenn sich wirklich etwas geändert hat
|
||||||
const prevMode = String(pendingAutoStartModeByKeyRef.current[key] ?? 'wait_public').trim()
|
if (sameUrl && sameMode && sameSource && sameRetryAt) {
|
||||||
const prevRetryAt = Number(pendingBlindRetryAtByKeyRef.current[key] ?? 0)
|
|
||||||
const nextShow = normalizePendingShow(show)
|
|
||||||
const nextRetryAt = mode === 'probe_retry'
|
|
||||||
? (Number.isFinite(nextProbeAtMs) ? Number(nextProbeAtMs) : 0)
|
|
||||||
: 0
|
|
||||||
|
|
||||||
const sameUrl = prevUrl === norm
|
|
||||||
const sameMode = prevMode === mode
|
|
||||||
const sameRetryAt =
|
|
||||||
mode !== 'probe_retry' || Math.abs(prevRetryAt - nextRetryAt) < 1000
|
|
||||||
|
|
||||||
if (sameUrl && sameMode && sameRetryAt) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2002,9 +2077,10 @@ export default function App() {
|
|||||||
modelKey: key,
|
modelKey: key,
|
||||||
url: norm,
|
url: norm,
|
||||||
mode,
|
mode,
|
||||||
nextProbeAtMs: mode === 'probe_retry' ? nextProbeAtMs : undefined,
|
nextProbeAtMs: mode === 'probe_retry' ? nextRetryAt : undefined,
|
||||||
currentShow: nextShow,
|
currentShow: nextShow,
|
||||||
imageUrl: imageUrl || undefined,
|
imageUrl: imageUrl || undefined,
|
||||||
|
source,
|
||||||
}),
|
}),
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
@ -2016,7 +2092,8 @@ export default function App() {
|
|||||||
const hadPending =
|
const hadPending =
|
||||||
Boolean(pendingAutoStartByKeyRef.current[key]) ||
|
Boolean(pendingAutoStartByKeyRef.current[key]) ||
|
||||||
Boolean(pendingAutoStartModeByKeyRef.current[key]) ||
|
Boolean(pendingAutoStartModeByKeyRef.current[key]) ||
|
||||||
Boolean(pendingBlindRetryAtByKeyRef.current[key])
|
Boolean(pendingBlindRetryAtByKeyRef.current[key]) ||
|
||||||
|
Boolean(pendingAutoStartSourceByKeyRef.current[key])
|
||||||
|
|
||||||
if (!hadPending) return
|
if (!hadPending) return
|
||||||
|
|
||||||
@ -2041,6 +2118,13 @@ export default function App() {
|
|||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setPendingAutoStartSourceByKey((prev) => {
|
||||||
|
const next = { ...(prev || {}) }
|
||||||
|
delete next[key]
|
||||||
|
pendingAutoStartSourceByKeyRef.current = next
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
setPendingWatchedRooms((prev) =>
|
setPendingWatchedRooms((prev) =>
|
||||||
prev.filter((x) => String(x.modelKey ?? '').trim().toLowerCase() !== key)
|
prev.filter((x) => String(x.modelKey ?? '').trim().toLowerCase() !== key)
|
||||||
)
|
)
|
||||||
@ -2170,7 +2254,6 @@ export default function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!recSettings.useChaturbateApi) {
|
if (!recSettings.useChaturbateApi) {
|
||||||
setCbOnlineFetchedAt('')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2253,6 +2336,9 @@ export default function App() {
|
|||||||
|
|
||||||
const mode = (modeMap[key] ?? 'wait_public') as PendingAutoStartMode
|
const mode = (modeMap[key] ?? 'wait_public') as PendingAutoStartMode
|
||||||
|
|
||||||
|
const source =
|
||||||
|
(pendingAutoStartSourceByKeyRef.current[key] ?? 'manual') as PendingAutoStartSource
|
||||||
|
|
||||||
if (snap.show === 'public') {
|
if (snap.show === 'public') {
|
||||||
enqueueStart({
|
enqueueStart({
|
||||||
url,
|
url,
|
||||||
@ -2272,6 +2358,7 @@ export default function App() {
|
|||||||
) {
|
) {
|
||||||
queuePendingAutoStart(key, url, snap.show, snap.imageUrl, {
|
queuePendingAutoStart(key, url, snap.show, snap.imageUrl, {
|
||||||
mode: 'wait_public',
|
mode: 'wait_public',
|
||||||
|
source,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -2287,6 +2374,7 @@ export default function App() {
|
|||||||
queuePendingAutoStart(key, url, snap.show, snap.imageUrl, {
|
queuePendingAutoStart(key, url, snap.show, snap.imageUrl, {
|
||||||
mode: 'probe_retry',
|
mode: 'probe_retry',
|
||||||
nextProbeAtMs: Date.now() + 60_000,
|
nextProbeAtMs: Date.now() + 60_000,
|
||||||
|
source,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2373,18 +2461,28 @@ export default function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authed || !recSettings.useChaturbateApi) {
|
if (!authed || !recSettings.useChaturbateApi) {
|
||||||
setCbOnlineFetchedAt('')
|
setCbOnlineFetchedAt('')
|
||||||
|
|
||||||
|
setHeaderStats((prev) => ({
|
||||||
|
...prev,
|
||||||
|
onlineModelsCount: 0,
|
||||||
|
onlineWatchedModelsCount: 0,
|
||||||
|
onlineFavCount: 0,
|
||||||
|
onlineLikedCount: 0,
|
||||||
|
}))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
let timer: number | null = null
|
let timer: number | null = null
|
||||||
|
let lastSeenFetchedAt = ''
|
||||||
|
|
||||||
const tick = async () => {
|
const tick = async (refresh = false) => {
|
||||||
try {
|
try {
|
||||||
const onlineRes = await fetch('/api/chaturbate/online', {
|
const onlineRes = await fetch(
|
||||||
method: 'GET',
|
refresh ? '/api/chaturbate/online?refresh=1' : '/api/chaturbate/online',
|
||||||
cache: 'no-store',
|
{ method: 'GET', cache: 'no-store' }
|
||||||
})
|
)
|
||||||
|
|
||||||
if (!onlineRes.ok) return
|
if (!onlineRes.ok) return
|
||||||
|
|
||||||
@ -2392,33 +2490,54 @@ export default function App() {
|
|||||||
if (cancelled || !data) return
|
if (cancelled || !data) return
|
||||||
|
|
||||||
const fetchedAt = String(data.fetchedAt ?? '').trim()
|
const fetchedAt = String(data.fetchedAt ?? '').trim()
|
||||||
if (fetchedAt && fetchedAt !== '0001-01-01T00:00:00Z') {
|
const safeFetchedAt =
|
||||||
setCbOnlineFetchedAt(fetchedAt)
|
fetchedAt && fetchedAt !== '0001-01-01T00:00:00Z' ? fetchedAt : ''
|
||||||
|
|
||||||
|
const totalRaw = Number(data.total ?? 0)
|
||||||
|
const total = Number.isFinite(totalRaw) && totalRaw >= 0 ? totalRaw : 0
|
||||||
|
|
||||||
|
// Online-Gesamtcounter sofort aus der Online-API aktualisieren
|
||||||
|
setHeaderStats((prev) => {
|
||||||
|
if (prev.onlineModelsCount === total) return prev
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
onlineModelsCount: total,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setCbOnlineFetchedAt(safeFetchedAt)
|
||||||
|
|
||||||
|
// Derselbe erfolgreiche Snapshot triggert den Refresh
|
||||||
|
// der watched/fav/like Counter + modelsByKey
|
||||||
|
if (safeFetchedAt && safeFetchedAt !== lastSeenFetchedAt) {
|
||||||
|
lastSeenFetchedAt = safeFetchedAt
|
||||||
|
void loadAppModelsSnapshot(requiredModelKeysRef.current)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void tick()
|
void tick(true)
|
||||||
|
|
||||||
timer = window.setInterval(() => {
|
timer = window.setInterval(() => {
|
||||||
void tick()
|
void tick(false)
|
||||||
}, 10_000)
|
}, 10_000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
if (timer != null) window.clearInterval(timer)
|
if (timer != null) window.clearInterval(timer)
|
||||||
}
|
}
|
||||||
}, [authed, recSettings.useChaturbateApi])
|
}, [authed, recSettings.useChaturbateApi, loadAppModelsSnapshot])
|
||||||
|
|
||||||
// ✅ StartURL (hier habe ich den alten Online-Fetch entfernt und nur Snapshot genutzt)
|
// ✅ StartURL (hier habe ich den alten Online-Fetch entfernt und nur Snapshot genutzt)
|
||||||
const startUrl = useCallback(async (rawUrl: string, opts?: { silent?: boolean }): Promise<boolean> => {
|
const startUrl = useCallback(async (rawUrl: string, opts?: { silent?: boolean }): Promise<boolean> => {
|
||||||
const norm0 = normalizeHttpUrl(rawUrl)
|
const norm0 = normalizeHttpUrl(rawUrl)
|
||||||
if (!norm0) return false
|
if (!norm0) return false
|
||||||
const norm = canonicalizeProviderUrl(norm0)
|
|
||||||
|
|
||||||
|
const norm = canonicalizeProviderUrl(norm0)
|
||||||
const silent = Boolean(opts?.silent)
|
const silent = Boolean(opts?.silent)
|
||||||
|
|
||||||
if (!silent) setError(null)
|
if (!silent) setError(null)
|
||||||
|
|
||||||
const provider = getProviderFromNormalizedUrl(norm)
|
const provider = getProviderFromNormalizedUrl(norm)
|
||||||
@ -2430,7 +2549,9 @@ export default function App() {
|
|||||||
const currentCookies = cookiesRef.current
|
const currentCookies = cookiesRef.current
|
||||||
|
|
||||||
if (provider === 'chaturbate' && !hasRequiredChaturbateCookies(currentCookies)) {
|
if (provider === 'chaturbate' && !hasRequiredChaturbateCookies(currentCookies)) {
|
||||||
if (!silent) setError('Für Chaturbate müssen die Cookies "cf_clearance" und "sessionId" gesetzt sein.')
|
if (!silent) {
|
||||||
|
setError('Für Chaturbate müssen die Cookies "cf_clearance" und "sessionId" gesetzt sein.')
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2438,7 +2559,7 @@ export default function App() {
|
|||||||
const alreadyRunning = jobsRef.current.some((j) => {
|
const alreadyRunning = jobsRef.current.some((j) => {
|
||||||
if (String(j.status || '').toLowerCase() !== 'running') return false
|
if (String(j.status || '').toLowerCase() !== 'running') return false
|
||||||
|
|
||||||
// ✅ Wenn endedAt existiert: Aufnahme ist fertig -> Postwork/Queue -> NICHT blocken
|
// Wenn endedAt existiert: Aufnahme ist fertig -> Postwork/Queue -> NICHT blocken
|
||||||
if ((j as any).endedAt) return false
|
if ((j as any).endedAt) return false
|
||||||
|
|
||||||
const jNorm0 = normalizeHttpUrl(String((j as any).sourceUrl || ''))
|
const jNorm0 = normalizeHttpUrl(String((j as any).sourceUrl || ''))
|
||||||
@ -2447,7 +2568,7 @@ export default function App() {
|
|||||||
})
|
})
|
||||||
if (alreadyRunning) return true
|
if (alreadyRunning) return true
|
||||||
|
|
||||||
// ✅ Chaturbate-Startlogik mit Online-API:
|
// Chaturbate-Startlogik mit Online-API:
|
||||||
// public -> direkt starten
|
// public -> direkt starten
|
||||||
// private/hidden/away -> wait_public
|
// private/hidden/away -> wait_public
|
||||||
// offline/unknown -> probe_retry mit hidden start
|
// offline/unknown -> probe_retry mit hidden start
|
||||||
@ -2472,7 +2593,10 @@ export default function App() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ q: [mkLower] }),
|
body: JSON.stringify({
|
||||||
|
q: [mkLower],
|
||||||
|
refresh: true,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (onlineRes.ok) {
|
if (onlineRes.ok) {
|
||||||
@ -2509,6 +2633,7 @@ export default function App() {
|
|||||||
// 2) in API + nicht public => warten bis public
|
// 2) in API + nicht public => warten bis public
|
||||||
queuePendingAutoStart(mkLower, norm, resolvedShow, resolvedImageUrl, {
|
queuePendingAutoStart(mkLower, norm, resolvedShow, resolvedImageUrl, {
|
||||||
mode: 'wait_public',
|
mode: 'wait_public',
|
||||||
|
source: 'manual',
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
@ -2516,6 +2641,7 @@ export default function App() {
|
|||||||
queuePendingAutoStart(mkLower, norm, resolvedShow, resolvedImageUrl, {
|
queuePendingAutoStart(mkLower, norm, resolvedShow, resolvedImageUrl, {
|
||||||
mode: 'probe_retry',
|
mode: 'probe_retry',
|
||||||
nextProbeAtMs: Date.now() + 60_000,
|
nextProbeAtMs: Date.now() + 60_000,
|
||||||
|
source: 'manual',
|
||||||
})
|
})
|
||||||
|
|
||||||
const ok = await doStartNow(norm, silent, { hidden: true })
|
const ok = await doStartNow(norm, silent, { hidden: true })
|
||||||
@ -2532,6 +2658,7 @@ export default function App() {
|
|||||||
queuePendingAutoStart(mkLower, norm, 'unknown', undefined, {
|
queuePendingAutoStart(mkLower, norm, 'unknown', undefined, {
|
||||||
mode: 'probe_retry',
|
mode: 'probe_retry',
|
||||||
nextProbeAtMs: Date.now() + 60_000,
|
nextProbeAtMs: Date.now() + 60_000,
|
||||||
|
source: 'manual',
|
||||||
})
|
})
|
||||||
|
|
||||||
const ok = await doStartNow(norm, silent, { hidden: true })
|
const ok = await doStartNow(norm, silent, { hidden: true })
|
||||||
@ -2552,23 +2679,36 @@ export default function App() {
|
|||||||
const created = await apiJSON<RecordJob>('/api/record', {
|
const created = await apiJSON<RecordJob>('/api/record', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ url: norm, cookie: cookieString }),
|
body: JSON.stringify({
|
||||||
|
url: norm,
|
||||||
|
cookie: cookieString,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✅ verhindert Doppel-Toast: StartUrl toastet ggf. schon selbst,
|
// Wichtig: model lokal sofort mitschleppen, damit overview / modelsByKey
|
||||||
|
// nicht erst nach SSE oder Dateinamen-Erkennung korrekt werden.
|
||||||
|
const localModelKey = providerKeyLowerFromUrl(norm)
|
||||||
|
const createdWithModel = localModelKey
|
||||||
|
? ({ ...created, model: localModelKey } as RecordJob)
|
||||||
|
: created
|
||||||
|
|
||||||
|
// verhindert Doppel-Toast: StartUrl toastet ggf. schon selbst,
|
||||||
// und kurz danach kommt der Job nochmal über SSE/polling rein.
|
// und kurz danach kommt der Job nochmal über SSE/polling rein.
|
||||||
if (created?.id) startedToastByJobIdRef.current[String(created.id)] = true
|
if ((createdWithModel as any)?.id) {
|
||||||
|
startedToastByJobIdRef.current[String((createdWithModel as any).id)] = true
|
||||||
|
}
|
||||||
|
|
||||||
setJobs((prev) => {
|
setJobs((prev) => {
|
||||||
const next = upsertJobById(prev, created)
|
const next = upsertJobById(prev, createdWithModel)
|
||||||
jobsRef.current = next
|
jobsRef.current = next
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const msg = e?.message ?? String(e)
|
const msg = e?.message ?? String(e)
|
||||||
|
|
||||||
// ✅ Spezialfall: Age-Gate / Cloudflare / kein Room-HTML => Cookies Hinweis
|
// Spezialfall: Age-Gate / Cloudflare / kein Room-HTML => Cookies Hinweis
|
||||||
if (isCookieGateError(msg)) {
|
if (isCookieGateError(msg)) {
|
||||||
showMissingCookiesMessage({ silent })
|
showMissingCookiesMessage({ silent })
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export default function LastUpdatedText({ fetchedAt, enabled = true }: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
|
|
||||||
const t = window.setInterval(() => setNow(Date.now()), 10_000)
|
const t = window.setInterval(() => setNow(Date.now()), 1_000)
|
||||||
return () => window.clearInterval(t)
|
return () => window.clearInterval(t)
|
||||||
}, [enabled])
|
}, [enabled])
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,35 @@ function regenerateTaskTitle(file: string) {
|
|||||||
return short ? `Assets neu generieren · ${short}` : 'Assets neu generieren'
|
return short ? `Assets neu generieren · ${short}` : 'Assets neu generieren'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assetsPhaseLabel(label?: string, phase?: string) {
|
||||||
|
const direct = String(label ?? '').trim()
|
||||||
|
if (direct) return direct
|
||||||
|
|
||||||
|
switch (String(phase ?? '').trim().toLowerCase()) {
|
||||||
|
case 'meta':
|
||||||
|
return 'Meta'
|
||||||
|
case 'thumb':
|
||||||
|
return 'Vorschaubild'
|
||||||
|
case 'teaser':
|
||||||
|
return 'Teaser'
|
||||||
|
case 'sprites':
|
||||||
|
return 'Sprites'
|
||||||
|
case 'analyze':
|
||||||
|
return 'KI-Analyse'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assetsTaskText(file?: string, label?: string, phase?: string) {
|
||||||
|
const fn = shortTaskFilename(file, 38)
|
||||||
|
const phaseText = assetsPhaseLabel(label, phase)
|
||||||
|
|
||||||
|
if (fn && phaseText) return `${fn} · ${phaseText}`
|
||||||
|
if (fn) return fn
|
||||||
|
return phaseText
|
||||||
|
}
|
||||||
|
|
||||||
export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||||
const [value, setValue] = useState<RecorderSettings>(DEFAULTS)
|
const [value, setValue] = useState<RecorderSettings>(DEFAULTS)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@ -224,13 +253,16 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
if (!alive || !data) return
|
if (!alive || !data) return
|
||||||
|
|
||||||
const assets = data.generateAssets
|
const assets = data.generateAssets
|
||||||
|
const assetsFile = String(assets?.currentFile || assets?.text || '').trim()
|
||||||
|
const assetsLabel = String(assets?.currentLabel || '').trim()
|
||||||
|
const assetsPhase = String(assets?.currentPhase || '').trim()
|
||||||
|
|
||||||
if (assets?.running) {
|
if (assets?.running) {
|
||||||
setAssetsTask((t) => ({
|
setAssetsTask((t) => ({
|
||||||
...t,
|
...t,
|
||||||
status: 'running',
|
status: 'running',
|
||||||
title: 'Assets generieren',
|
title: 'Assets generieren',
|
||||||
text: shortTaskFilename(String(assets.currentFile || assets.text || '')),
|
text: assetsTaskText(assetsFile, assetsLabel, assetsPhase),
|
||||||
done: Number(assets.done ?? 0),
|
done: Number(assets.done ?? 0),
|
||||||
total: Number(assets.total ?? 0),
|
total: Number(assets.total ?? 0),
|
||||||
err: undefined,
|
err: undefined,
|
||||||
@ -246,7 +278,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
assetsError.toLowerCase() === 'canceled'
|
assetsError.toLowerCase() === 'canceled'
|
||||||
|
|
||||||
setAssetsTask((t) => {
|
setAssetsTask((t) => {
|
||||||
// Wenn schon idle oder bereits am Ausfaden: nicht wieder "sichtbar reaktivieren"
|
|
||||||
if (cancelled && (t.status === 'idle' || t.fading)) {
|
if (cancelled && (t.status === 'idle' || t.fading)) {
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
@ -255,7 +286,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
...t,
|
...t,
|
||||||
status: cancelled ? 'cancelled' : 'error',
|
status: cancelled ? 'cancelled' : 'error',
|
||||||
title: 'Assets generieren',
|
title: 'Assets generieren',
|
||||||
text: shortTaskFilename(String(assets.currentFile || assets.text || '')),
|
text: assetsTaskText(assetsFile, assetsLabel, assetsPhase),
|
||||||
done: Number(assets.done ?? 0),
|
done: Number(assets.done ?? 0),
|
||||||
total: Number(assets.total ?? 0),
|
total: Number(assets.total ?? 0),
|
||||||
err: cancelled ? undefined : assetsError,
|
err: cancelled ? undefined : assetsError,
|
||||||
@ -270,25 +301,13 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
...t,
|
...t,
|
||||||
status: 'done',
|
status: 'done',
|
||||||
title: 'Assets generieren',
|
title: 'Assets generieren',
|
||||||
text: shortTaskFilename(String(assets.currentFile || assets.text || '')),
|
text: assetsTaskText(assetsFile, assetsLabel, assetsPhase),
|
||||||
done: Number(assets.done ?? 0),
|
done: Number(assets.done ?? 0),
|
||||||
total: Number(assets.total ?? 0),
|
total: Number(assets.total ?? 0),
|
||||||
err: undefined,
|
err: undefined,
|
||||||
cancellable: false,
|
cancellable: false,
|
||||||
fading: false,
|
fading: false,
|
||||||
}))
|
}))
|
||||||
} else {
|
|
||||||
setAssetsTask((t) => ({
|
|
||||||
...t,
|
|
||||||
status: 'idle',
|
|
||||||
title: 'Assets generieren',
|
|
||||||
text: '',
|
|
||||||
done: 0,
|
|
||||||
total: 0,
|
|
||||||
err: undefined,
|
|
||||||
cancellable: true,
|
|
||||||
fading: false,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const regenJobs = Array.isArray(data?.regenerateAssetsJobs)
|
const regenJobs = Array.isArray(data?.regenerateAssetsJobs)
|
||||||
@ -870,7 +889,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
<Task
|
<Task
|
||||||
title="Assets-Generator"
|
title="Assets-Generator"
|
||||||
description="Erzeugt fehlende Assets (thumb/preview/meta). Fortschritt & Abbrechen oben in der Taskliste."
|
description="Erzeugt nur fehlende oder unvollständige Assets (Meta, Vorschaubild, Teaser, Sprites, KI-Analyse). Bereits vorhandene Assets werden übersprungen."
|
||||||
startLabel="Start"
|
startLabel="Start"
|
||||||
startingLabel="Starte…"
|
startingLabel="Starte…"
|
||||||
startUrl="/api/tasks/generate-assets"
|
startUrl="/api/tasks/generate-assets"
|
||||||
@ -891,13 +910,15 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
onProgress={(p) => {
|
onProgress={(p) => {
|
||||||
const fn = shortTaskFilename(p.currentFile)
|
const currentFile = String((p as any).currentFile ?? '').trim()
|
||||||
|
const currentLabel = String((p as any).currentLabel ?? '').trim()
|
||||||
|
const currentPhase = String((p as any).currentPhase ?? '').trim()
|
||||||
|
|
||||||
setAssetsTask((t: TaskItem) => ({
|
setAssetsTask((t: TaskItem) => ({
|
||||||
...t,
|
...t,
|
||||||
status: 'running',
|
status: 'running',
|
||||||
title: 'Assets generieren',
|
title: 'Assets generieren',
|
||||||
text: fn || '',
|
text: assetsTaskText(currentFile, currentLabel, currentPhase),
|
||||||
done: p.done,
|
done: p.done,
|
||||||
total: p.total,
|
total: p.total,
|
||||||
}))
|
}))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user