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
|
||||
}
|
||||
|
||||
if _, err := ensureTeaserForVideoCtx(ctx, videoPath, sourceURL); err != nil {
|
||||
return fmt.Errorf("deferred teaser failed for %s: %w", videoPath, err)
|
||||
}
|
||||
|
||||
// NUR noch langsame Background-Schritte:
|
||||
// 1) Sprite
|
||||
if _, err := ensureSpritesForVideoCtx(ctx, videoPath, sourceURL); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// 2) KI-Analyse
|
||||
analyzeReady, err := ensureAnalyzeForVideoCtx(ctx, videoPath, sourceURL, "nsfw")
|
||||
if err != nil {
|
||||
return fmt.Errorf("deferred analyze failed for %s: %w", videoPath, err)
|
||||
|
||||
@ -117,7 +117,7 @@ func generatePreviewSpriteJPG(
|
||||
"-i", videoPath,
|
||||
"-an",
|
||||
"-sn",
|
||||
"-threads", "0",
|
||||
"-threads", "2",
|
||||
"-vf", vf,
|
||||
"-frames:v", "1",
|
||||
"-update", "1",
|
||||
|
||||
@ -275,6 +275,7 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
||||
}
|
||||
|
||||
showByUser := map[string]string{}
|
||||
imageByUser := map[string]string{}
|
||||
seenInAPI := map[string]bool{}
|
||||
|
||||
for _, r := range rooms {
|
||||
@ -283,19 +284,26 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
||||
continue
|
||||
}
|
||||
seenInAPI[u] = true
|
||||
showByUser[u] = strings.ToLower(strings.TrimSpace(r.CurrentShow))
|
||||
showByUser[u] = normalizePendingShowServer(r.CurrentShow)
|
||||
imageByUser[u] = selectBestRoomImageURL(r)
|
||||
}
|
||||
|
||||
// laufende Jobs sammeln
|
||||
running := map[string]bool{}
|
||||
runningByUser := map[string]*RecordJob{}
|
||||
jobsMu.RLock()
|
||||
for _, j := range jobs {
|
||||
if j == nil {
|
||||
continue
|
||||
}
|
||||
if !isActiveRecordingJob(j) {
|
||||
continue
|
||||
}
|
||||
if detectProvider(strings.TrimSpace(j.SourceURL)) != "chaturbate" {
|
||||
continue
|
||||
}
|
||||
u := chaturbateUserFromURL(j.SourceURL)
|
||||
if u != "" {
|
||||
running[u] = true
|
||||
runningByUser[u] = j
|
||||
}
|
||||
}
|
||||
jobsMu.RUnlock()
|
||||
@ -348,7 +356,7 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if running[it.userKey] {
|
||||
if runningByUser[it.userKey] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -364,19 +372,21 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
||||
queued = nextQueued
|
||||
|
||||
now := time.Now()
|
||||
nextPending := make([]PendingAutoStartItem, 0, len(watchedByUser))
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 1) watched + API sagt public => normal enqueue
|
||||
// ------------------------------------------------------------
|
||||
for user, m := range watchedByUser {
|
||||
show := strings.ToLower(strings.TrimSpace(showByUser[user]))
|
||||
if show == "" {
|
||||
u := resolveChaturbateURL(m)
|
||||
if u == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(show, "public") {
|
||||
continue
|
||||
}
|
||||
if running[user] {
|
||||
|
||||
show := normalizePendingShowServer(showByUser[user])
|
||||
img := strings.TrimSpace(imageByUser[user])
|
||||
|
||||
switch show {
|
||||
case "public":
|
||||
// public => kein Pending-Eintrag
|
||||
if runningByUser[user] != nil {
|
||||
continue
|
||||
}
|
||||
if queued[user] {
|
||||
@ -386,26 +396,42 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
||||
continue
|
||||
}
|
||||
|
||||
u := resolveChaturbateURL(m)
|
||||
if u == "" {
|
||||
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})
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 2) watched + NICHT in API => blind best-effort try vormerken
|
||||
// ------------------------------------------------------------
|
||||
for user, m := range watchedByUser {
|
||||
if seenInAPI[user] {
|
||||
continue
|
||||
}
|
||||
if running[user] {
|
||||
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] {
|
||||
@ -415,17 +441,17 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
||||
continue
|
||||
}
|
||||
|
||||
u := resolveChaturbateURL(m)
|
||||
if u == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
queue = append(queue, autoStartItem{
|
||||
userKey: user,
|
||||
url: u,
|
||||
})
|
||||
queued[user] = true
|
||||
}
|
||||
}
|
||||
|
||||
pendingAutoStartMu.Lock()
|
||||
_ = saveWatchedPendingAutoStartItems(nextPending)
|
||||
pendingAutoStartMu.Unlock()
|
||||
|
||||
case <-startTicker.C:
|
||||
if isAutostartPaused() {
|
||||
@ -485,6 +511,10 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
||||
continue
|
||||
}
|
||||
|
||||
pendingAutoStartMu.Lock()
|
||||
_ = removeWatchedPendingAutoStartItem(it.userKey)
|
||||
pendingAutoStartMu.Unlock()
|
||||
|
||||
if verboseLogs() {
|
||||
fmt.Println("▶️ [autostart] started:", it.url)
|
||||
}
|
||||
|
||||
@ -672,7 +672,8 @@ type cbOnlineOutRoom struct {
|
||||
|
||||
type cbOnlineResponse struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
FetchedAt time.Time `json:"fetchedAt"`
|
||||
FetchedAt time.Time `json:"fetchedAt"` // letzter erfolgreicher Snapshot
|
||||
LastAttempt time.Time `json:"lastAttempt"` // letzter Pull-Versuch
|
||||
Count int `json:"count"`
|
||||
Total int `json:"total"`
|
||||
LastError string `json:"lastError"`
|
||||
@ -696,12 +697,6 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
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)
|
||||
// ---------------------------
|
||||
@ -837,25 +832,24 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
||||
b := (qIsHD == "1" || qIsHD == "true" || qIsHD == "yes")
|
||||
isHD = &b
|
||||
}
|
||||
|
||||
allowedShow = toSet(shows)
|
||||
}
|
||||
|
||||
onlySpecificUsers := len(users) > 0
|
||||
|
||||
// ---------------------------
|
||||
// Response Cache (2s)
|
||||
// Response Cache
|
||||
// ---------------------------
|
||||
cacheKey := "cb_online:" + hashKey(
|
||||
fmt.Sprintf("enabled=%v", enabled),
|
||||
"users="+strings.Join(users, ","),
|
||||
"show="+strings.Join(keysOfSet(allowedShow), ","),
|
||||
|
||||
"gender="+strings.Join(keysOfSet(allowedGender), ","),
|
||||
"country="+strings.Join(keysOfSet(allowedCountry), ","),
|
||||
"tagsAny="+strings.Join(keysOfSet(allowedTagsAny), ","),
|
||||
"minUsers="+derefInt(minUsers),
|
||||
"isHD="+derefBool(isHD),
|
||||
|
||||
fmt.Sprintf("refresh=%v", wantRefresh),
|
||||
"lite=1",
|
||||
)
|
||||
@ -876,6 +870,7 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
||||
out := cbOnlineResponse{
|
||||
Enabled: false,
|
||||
FetchedAt: time.Time{},
|
||||
LastAttempt: time.Time{},
|
||||
Count: 0,
|
||||
Total: 0,
|
||||
LastError: "",
|
||||
@ -927,7 +922,7 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
cbRefreshMu.Unlock()
|
||||
} else if needBootstrap && time.Since(lastAttempt) >= bootstrapCooldown {
|
||||
// ✅ Bootstrap darf weiterhin asynchron bleiben
|
||||
// Bootstrap weiterhin asynchron
|
||||
cbRefreshMu.Lock()
|
||||
if cbRefreshInFlight {
|
||||
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()
|
||||
fetchedAt = cb.FetchedAt
|
||||
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 {
|
||||
if len(allowedShow) > 0 {
|
||||
s := strings.ToLower(strings.TrimSpace(rm.CurrentShow))
|
||||
@ -1067,6 +1061,7 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
||||
out := cbOnlineResponse{
|
||||
Enabled: true,
|
||||
FetchedAt: fetchedAt,
|
||||
LastAttempt: lastAttempt,
|
||||
Count: len(outRooms),
|
||||
Total: total,
|
||||
LastError: lastErr,
|
||||
|
||||
@ -21,6 +21,7 @@ type PendingAutoStartItem struct {
|
||||
NextProbeAtMs int64 `json:"nextProbeAtMs,omitempty"` // nur bei probe_retry
|
||||
CurrentShow string `json:"currentShow,omitempty"` // public/private/hidden/away/offline/unknown
|
||||
ImageURL string `json:"imageUrl,omitempty"`
|
||||
Source string `json:"source,omitempty"` // manual | watched
|
||||
}
|
||||
|
||||
type PendingAutoStartResponse struct {
|
||||
@ -33,6 +34,17 @@ type pendingAutoStartFile struct {
|
||||
|
||||
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 {
|
||||
if strings.TrimSpace(strings.ToLower(v)) == "probe_retry" {
|
||||
return "probe_retry"
|
||||
@ -103,12 +115,94 @@ func loadPendingAutoStartItems(userKey string) ([]PendingAutoStartItem, error) {
|
||||
NextProbeAtMs: it.NextProbeAtMs,
|
||||
CurrentShow: normalizePendingShowServer(it.CurrentShow),
|
||||
ImageURL: strings.TrimSpace(it.ImageURL),
|
||||
Source: normalizePendingSourceServer(it.Source),
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
path := pendingAutoStartFilePath(userKey)
|
||||
|
||||
@ -148,7 +242,7 @@ func handlePendingAutoStart(auth *AuthManager) http.HandlerFunc {
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
items, err := loadPendingAutoStartItems(userKey)
|
||||
items, err := loadMergedPendingAutoStartItems(userKey)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@ -172,6 +266,7 @@ func handlePendingAutoStart(auth *AuthManager) http.HandlerFunc {
|
||||
in.Mode = normalizePendingModeServer(in.Mode)
|
||||
in.CurrentShow = normalizePendingShowServer(in.CurrentShow)
|
||||
in.ImageURL = strings.TrimSpace(in.ImageURL)
|
||||
in.Source = normalizePendingSourceServer(in.Source)
|
||||
|
||||
if in.ModelKey == "" {
|
||||
http.Error(w, "missing modelKey", http.StatusBadRequest)
|
||||
|
||||
@ -2310,6 +2310,7 @@ func generateTeaserMP4(ctx context.Context, srcPath, outPath string, startSec, d
|
||||
|
||||
"-movflags", "+faststart",
|
||||
"-f", "mp4",
|
||||
"-threads", "2",
|
||||
tmp,
|
||||
)
|
||||
|
||||
|
||||
@ -1473,6 +1473,7 @@ func recordStatus(w http.ResponseWriter, r *http.Request) {
|
||||
jobsMu.RUnlock()
|
||||
|
||||
applyPreviewSpriteTruthToRecordJobMeta(&c)
|
||||
applyFinishedPhaseTruthToRecordJobMeta(&c)
|
||||
respondJSON(w, &c)
|
||||
}
|
||||
|
||||
@ -2300,7 +2301,13 @@ func recordDoneList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
c := *base
|
||||
|
||||
// Sprite-Infos aus Dateisystem/meta ergänzen
|
||||
applyPreviewSpriteTruthToRecordJobMeta(&c)
|
||||
|
||||
// WICHTIG: completed-Flags für Meta/Thumb/Teaser/Sprites/Analyze setzen
|
||||
applyFinishedPhaseTruthToRecordJobMeta(&c)
|
||||
|
||||
out = append(out, &c)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -257,50 +257,37 @@ func runGenerateMissingAssets(ctx context.Context) {
|
||||
}
|
||||
|
||||
// Initialisierung: Total etc. + SSE Push
|
||||
updateAssetsState(func(st *AssetsTaskState) {
|
||||
st.Total = len(items)
|
||||
st.Done = 0
|
||||
st.GeneratedThumbs = 0
|
||||
st.GeneratedPreviews = 0
|
||||
st.Skipped = 0
|
||||
st.Error = ""
|
||||
})
|
||||
type assetCandidate struct {
|
||||
name string
|
||||
path string
|
||||
id string
|
||||
beforeTruth finishedPhaseTruth
|
||||
}
|
||||
|
||||
for i, it := range items {
|
||||
candidates := make([]assetCandidate, 0, len(items))
|
||||
skippedCount := 0
|
||||
|
||||
for _, it := range items {
|
||||
if err := ctx.Err(); err != nil {
|
||||
finishWithErr(err)
|
||||
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))
|
||||
id := stripHotPrefix(base)
|
||||
if strings.TrimSpace(id) == "" {
|
||||
clearAssetsTaskFinishedPostworkStates(it.name)
|
||||
|
||||
updateAssetsState(func(st *AssetsTaskState) {
|
||||
st.Done = i + 1
|
||||
})
|
||||
skippedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// Datei-Info (validieren)
|
||||
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)
|
||||
|
||||
updateAssetsState(func(st *AssetsTaskState) {
|
||||
st.Done = i + 1
|
||||
})
|
||||
skippedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// Vorher-Wahrheit ermitteln: was fehlt wirklich?
|
||||
beforeTruth := finishedPhaseTruthForID(id)
|
||||
|
||||
needMeta := !beforeTruth.MetaReady
|
||||
@ -309,17 +296,69 @@ func runGenerateMissingAssets(ctx context.Context) {
|
||||
needSprites := !beforeTruth.SpritesReady
|
||||
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 {
|
||||
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) {
|
||||
st.Skipped++
|
||||
st.Done = i + 1
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
beforeTruth := it.beforeTruth
|
||||
|
||||
needMeta := !beforeTruth.MetaReady
|
||||
needThumb := !beforeTruth.ThumbReady
|
||||
needTeaser := !beforeTruth.TeaserReady
|
||||
needSprites := !beforeTruth.SpritesReady
|
||||
needAnalyze := !beforeTruth.AnalyzeReady
|
||||
|
||||
// Pfade einmalig über zentralen Helper
|
||||
_, _, _, _, metaPath, perr := assetPathsForID(id)
|
||||
if perr != nil {
|
||||
|
||||
@ -190,6 +190,7 @@ type ChaturbateOnlineRoomItem = {
|
||||
type ChaturbateOnlineResponse = {
|
||||
enabled?: boolean
|
||||
fetchedAt?: string
|
||||
lastAttempt?: string
|
||||
count?: number
|
||||
total?: number
|
||||
lastError?: string
|
||||
@ -204,6 +205,8 @@ type AutostartState = {
|
||||
|
||||
type PendingAutoStartMode = 'wait_public' | 'probe_retry'
|
||||
|
||||
type PendingAutoStartSource = 'manual' | 'watched'
|
||||
|
||||
type PendingAutoStartItem = {
|
||||
modelKey: string
|
||||
url: string
|
||||
@ -211,6 +214,7 @@ type PendingAutoStartItem = {
|
||||
nextProbeAtMs?: number
|
||||
currentShow?: 'public' | 'private' | 'hidden' | 'away' | 'offline' | 'unknown'
|
||||
imageUrl?: string
|
||||
source?: PendingAutoStartSource
|
||||
}
|
||||
|
||||
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}/
|
||||
|
||||
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 norm0 = normalizeHttpUrl(raw)
|
||||
const norm = norm0 ? canonicalizeProviderUrl(norm0) : ''
|
||||
@ -400,7 +409,9 @@ function chaturbateModelKeyFromJob(job: RecordJob): string {
|
||||
const keyFromUrl = providerKeyLowerFromUrl(norm)
|
||||
if (keyFromUrl) return keyFromUrl
|
||||
|
||||
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '').trim().toLowerCase()
|
||||
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return keyFromFile
|
||||
}
|
||||
|
||||
@ -418,6 +429,11 @@ function modelKeyFromFilename(fileOrPath: string): string | null {
|
||||
}
|
||||
|
||||
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 norm0 = normalizeHttpUrl(raw)
|
||||
const norm = norm0 ? canonicalizeProviderUrl(norm0) : ''
|
||||
@ -425,7 +441,9 @@ function modelEventKeyFromJob(job: RecordJob): string {
|
||||
const keyFromUrl = providerKeyLowerFromUrl(norm)
|
||||
if (keyFromUrl) return keyFromUrl
|
||||
|
||||
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '').trim().toLowerCase()
|
||||
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return keyFromFile
|
||||
}
|
||||
|
||||
@ -435,7 +453,9 @@ function modelEventKeyFromDoneJob(job: RecordJob): 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 = ''
|
||||
try {
|
||||
@ -449,6 +469,14 @@ function modelKeyAndHostFromJob(job: RecordJob): { modelKey: string; host: strin
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (direct) {
|
||||
return { modelKey: direct, host }
|
||||
}
|
||||
|
||||
const keyFromFile = (modelKeyFromFilename((job as any)?.output || '') || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
if (keyFromFile) {
|
||||
return { modelKey: keyFromFile, host }
|
||||
}
|
||||
@ -583,6 +611,13 @@ export default function App() {
|
||||
const [pendingAutoStartModeByKey, setPendingAutoStartModeByKey] = useState<Record<string, PendingAutoStartMode>>({})
|
||||
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)
|
||||
useEffect(() => {
|
||||
pendingAutoStartModeByKeyRef.current = pendingAutoStartModeByKey
|
||||
@ -687,10 +722,12 @@ export default function App() {
|
||||
setPendingAutoStartByKey({})
|
||||
setPendingAutoStartModeByKey({})
|
||||
setPendingBlindRetryAtByKey({})
|
||||
setPendingAutoStartSourceByKey({})
|
||||
|
||||
pendingAutoStartByKeyRef.current = {}
|
||||
pendingAutoStartModeByKeyRef.current = {}
|
||||
pendingBlindRetryAtByKeyRef.current = {}
|
||||
pendingAutoStartSourceByKeyRef.current = {}
|
||||
|
||||
// optional: URL-Feld leeren
|
||||
setSourceUrl('')
|
||||
@ -924,12 +961,15 @@ export default function App() {
|
||||
const nextByKey: Record<string, string> = {}
|
||||
const nextModeByKey: Record<string, PendingAutoStartMode> = {}
|
||||
const nextBlindRetryAtByKey: Record<string, number> = {}
|
||||
const nextSourceByKey: Record<string, PendingAutoStartSource> = {}
|
||||
|
||||
for (const item of items) {
|
||||
const keyLower = String(item?.modelKey ?? '').trim().toLowerCase()
|
||||
const norm0 = normalizeHttpUrl(String(item?.url ?? ''))
|
||||
const mode: PendingAutoStartMode =
|
||||
String(item?.mode ?? '').trim() === 'probe_retry' ? 'probe_retry' : 'wait_public'
|
||||
const source: PendingAutoStartSource =
|
||||
String(item?.source ?? '').trim() === 'watched' ? 'watched' : 'manual'
|
||||
|
||||
if (!keyLower || !norm0) continue
|
||||
|
||||
@ -937,6 +977,7 @@ export default function App() {
|
||||
|
||||
nextByKey[keyLower] = norm
|
||||
nextModeByKey[keyLower] = mode
|
||||
nextSourceByKey[keyLower] = source
|
||||
|
||||
if (mode === 'probe_retry') {
|
||||
const ts = Number(item?.nextProbeAtMs ?? 0)
|
||||
@ -953,6 +994,9 @@ export default function App() {
|
||||
|
||||
setPendingBlindRetryAtByKey(nextBlindRetryAtByKey)
|
||||
pendingBlindRetryAtByKeyRef.current = nextBlindRetryAtByKey
|
||||
|
||||
setPendingAutoStartSourceByKey(nextSourceByKey)
|
||||
pendingAutoStartSourceByKeyRef.current = nextSourceByKey
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
@ -1072,6 +1116,8 @@ export default function App() {
|
||||
return {
|
||||
...(prevJob ?? ({} as RecordJob)),
|
||||
id: jobId,
|
||||
model: msg.model ?? (prevJob as any)?.model,
|
||||
|
||||
status: (msg.status as any) ?? (prevJob?.status ?? 'running'),
|
||||
sourceUrl: msg.sourceUrl ?? ((prevJob as any)?.sourceUrl ?? ''),
|
||||
output: msg.output ?? (prevJob?.output ?? ''),
|
||||
@ -1681,6 +1727,7 @@ export default function App() {
|
||||
const alreadyRunning = jobsRef.current.some((j) => {
|
||||
if (String(j.status || '').toLowerCase() !== 'running') return false
|
||||
if ((j as any).endedAt) return false
|
||||
|
||||
const jNorm0 = normalizeHttpUrl(String((j as any).sourceUrl || ''))
|
||||
const jNorm = jNorm0 ? canonicalizeProviderUrl(jNorm0) : ''
|
||||
return jNorm === normUrl
|
||||
@ -1697,7 +1744,9 @@ export default function App() {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -1715,13 +1764,20 @@ export default function App() {
|
||||
}),
|
||||
})
|
||||
|
||||
if (created?.id && !opts?.hidden) {
|
||||
startedToastByJobIdRef.current[String(created.id)] = true
|
||||
// Wichtig: model lokal sofort mitschleppen, damit requiredModelKeys /
|
||||
// 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) => {
|
||||
const next = upsertJobById(prev, created)
|
||||
const next = upsertJobById(prev, createdWithModel)
|
||||
jobsRef.current = next
|
||||
return next
|
||||
})
|
||||
@ -1833,11 +1889,17 @@ export default function App() {
|
||||
[requiredModelKeys]
|
||||
)
|
||||
|
||||
|
||||
|
||||
const lastOverviewSigRef = useRef<string>('')
|
||||
const requiredModelKeysRef = useRef<string[]>(requiredModelKeys)
|
||||
|
||||
useEffect(() => {
|
||||
requiredModelKeysRef.current = requiredModelKeys
|
||||
}, [requiredModelKeys])
|
||||
|
||||
const lastOverviewSigRef = useRef<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!authed) return
|
||||
|
||||
const scheduleOverviewLoad = () => {
|
||||
if (overviewTimerRef.current != null) {
|
||||
window.clearTimeout(overviewTimerRef.current)
|
||||
@ -1870,8 +1932,6 @@ export default function App() {
|
||||
} catch {}
|
||||
|
||||
setPlayerModel((prev) => (prev?.id === updated.id ? updated : prev))
|
||||
|
||||
// counts/headerStats trotzdem später sauber nachziehen
|
||||
scheduleOverviewLoad()
|
||||
return
|
||||
}
|
||||
@ -1908,7 +1968,7 @@ export default function App() {
|
||||
overviewTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
||||
}, [authed, loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
||||
|
||||
const queuePendingAutoStart = useCallback((
|
||||
keyLower: string,
|
||||
@ -1918,6 +1978,7 @@ export default function App() {
|
||||
opts?: {
|
||||
mode?: PendingAutoStartMode
|
||||
nextProbeAtMs?: number
|
||||
source?: PendingAutoStartSource
|
||||
}
|
||||
) => {
|
||||
const key = String(keyLower ?? '').trim().toLowerCase()
|
||||
@ -1926,11 +1987,31 @@ export default function App() {
|
||||
|
||||
const norm = canonicalizeProviderUrl(norm0)
|
||||
const mode: PendingAutoStartMode = opts?.mode ?? 'wait_public'
|
||||
const source: PendingAutoStartSource = opts?.source === 'watched' ? 'watched' : 'manual'
|
||||
|
||||
const nextProbeAtMs =
|
||||
mode === 'probe_retry'
|
||||
? Number(opts?.nextProbeAtMs ?? (Date.now() + 60_000))
|
||||
: 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) => {
|
||||
const next = { ...(prev || {}), [key]: norm }
|
||||
pendingAutoStartByKeyRef.current = next
|
||||
@ -1943,11 +2024,17 @@ export default function App() {
|
||||
return next
|
||||
})
|
||||
|
||||
setPendingAutoStartSourceByKey((prev) => {
|
||||
const next = { ...(prev || {}), [key]: source }
|
||||
pendingAutoStartSourceByKeyRef.current = next
|
||||
return next
|
||||
})
|
||||
|
||||
setPendingBlindRetryAtByKey((prev) => {
|
||||
const next = { ...(prev || {}) }
|
||||
|
||||
if (mode === 'probe_retry') {
|
||||
next[key] = Number.isFinite(nextProbeAtMs) ? Number(nextProbeAtMs) : Date.now() + 60_000
|
||||
next[key] = nextRetryAt || (Date.now() + 60_000)
|
||||
} else {
|
||||
delete next[key]
|
||||
}
|
||||
@ -1961,7 +2048,7 @@ export default function App() {
|
||||
id: key,
|
||||
modelKey: key,
|
||||
url: norm,
|
||||
currentShow: normalizePendingShow(show),
|
||||
currentShow: nextShow,
|
||||
imageUrl: imageUrl || undefined,
|
||||
}
|
||||
|
||||
@ -1978,20 +2065,8 @@ export default function App() {
|
||||
return [nextItem, ...prev]
|
||||
})
|
||||
|
||||
const prevUrl = String(pendingAutoStartByKeyRef.current[key] ?? '').trim()
|
||||
const prevMode = String(pendingAutoStartModeByKeyRef.current[key] ?? 'wait_public').trim()
|
||||
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) {
|
||||
// ✅ Nur persistieren, wenn sich wirklich etwas geändert hat
|
||||
if (sameUrl && sameMode && sameSource && sameRetryAt) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -2002,9 +2077,10 @@ export default function App() {
|
||||
modelKey: key,
|
||||
url: norm,
|
||||
mode,
|
||||
nextProbeAtMs: mode === 'probe_retry' ? nextProbeAtMs : undefined,
|
||||
nextProbeAtMs: mode === 'probe_retry' ? nextRetryAt : undefined,
|
||||
currentShow: nextShow,
|
||||
imageUrl: imageUrl || undefined,
|
||||
source,
|
||||
}),
|
||||
}).catch(() => {})
|
||||
}, [])
|
||||
@ -2016,7 +2092,8 @@ export default function App() {
|
||||
const hadPending =
|
||||
Boolean(pendingAutoStartByKeyRef.current[key]) ||
|
||||
Boolean(pendingAutoStartModeByKeyRef.current[key]) ||
|
||||
Boolean(pendingBlindRetryAtByKeyRef.current[key])
|
||||
Boolean(pendingBlindRetryAtByKeyRef.current[key]) ||
|
||||
Boolean(pendingAutoStartSourceByKeyRef.current[key])
|
||||
|
||||
if (!hadPending) return
|
||||
|
||||
@ -2041,6 +2118,13 @@ export default function App() {
|
||||
return next
|
||||
})
|
||||
|
||||
setPendingAutoStartSourceByKey((prev) => {
|
||||
const next = { ...(prev || {}) }
|
||||
delete next[key]
|
||||
pendingAutoStartSourceByKeyRef.current = next
|
||||
return next
|
||||
})
|
||||
|
||||
setPendingWatchedRooms((prev) =>
|
||||
prev.filter((x) => String(x.modelKey ?? '').trim().toLowerCase() !== key)
|
||||
)
|
||||
@ -2170,7 +2254,6 @@ export default function App() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!recSettings.useChaturbateApi) {
|
||||
setCbOnlineFetchedAt('')
|
||||
return
|
||||
}
|
||||
|
||||
@ -2253,6 +2336,9 @@ export default function App() {
|
||||
|
||||
const mode = (modeMap[key] ?? 'wait_public') as PendingAutoStartMode
|
||||
|
||||
const source =
|
||||
(pendingAutoStartSourceByKeyRef.current[key] ?? 'manual') as PendingAutoStartSource
|
||||
|
||||
if (snap.show === 'public') {
|
||||
enqueueStart({
|
||||
url,
|
||||
@ -2272,6 +2358,7 @@ export default function App() {
|
||||
) {
|
||||
queuePendingAutoStart(key, url, snap.show, snap.imageUrl, {
|
||||
mode: 'wait_public',
|
||||
source,
|
||||
})
|
||||
continue
|
||||
}
|
||||
@ -2287,6 +2374,7 @@ export default function App() {
|
||||
queuePendingAutoStart(key, url, snap.show, snap.imageUrl, {
|
||||
mode: 'probe_retry',
|
||||
nextProbeAtMs: Date.now() + 60_000,
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
@ -2373,18 +2461,28 @@ export default function App() {
|
||||
useEffect(() => {
|
||||
if (!authed || !recSettings.useChaturbateApi) {
|
||||
setCbOnlineFetchedAt('')
|
||||
|
||||
setHeaderStats((prev) => ({
|
||||
...prev,
|
||||
onlineModelsCount: 0,
|
||||
onlineWatchedModelsCount: 0,
|
||||
onlineFavCount: 0,
|
||||
onlineLikedCount: 0,
|
||||
}))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
let timer: number | null = null
|
||||
let lastSeenFetchedAt = ''
|
||||
|
||||
const tick = async () => {
|
||||
const tick = async (refresh = false) => {
|
||||
try {
|
||||
const onlineRes = await fetch('/api/chaturbate/online', {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
})
|
||||
const onlineRes = await fetch(
|
||||
refresh ? '/api/chaturbate/online?refresh=1' : '/api/chaturbate/online',
|
||||
{ method: 'GET', cache: 'no-store' }
|
||||
)
|
||||
|
||||
if (!onlineRes.ok) return
|
||||
|
||||
@ -2392,33 +2490,54 @@ export default function App() {
|
||||
if (cancelled || !data) return
|
||||
|
||||
const fetchedAt = String(data.fetchedAt ?? '').trim()
|
||||
if (fetchedAt && fetchedAt !== '0001-01-01T00:00:00Z') {
|
||||
setCbOnlineFetchedAt(fetchedAt)
|
||||
const safeFetchedAt =
|
||||
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 {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
void tick()
|
||||
void tick(true)
|
||||
|
||||
timer = window.setInterval(() => {
|
||||
void tick()
|
||||
void tick(false)
|
||||
}, 10_000)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
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)
|
||||
const startUrl = useCallback(async (rawUrl: string, opts?: { silent?: boolean }): Promise<boolean> => {
|
||||
const norm0 = normalizeHttpUrl(rawUrl)
|
||||
if (!norm0) return false
|
||||
const norm = canonicalizeProviderUrl(norm0)
|
||||
|
||||
const norm = canonicalizeProviderUrl(norm0)
|
||||
const silent = Boolean(opts?.silent)
|
||||
|
||||
if (!silent) setError(null)
|
||||
|
||||
const provider = getProviderFromNormalizedUrl(norm)
|
||||
@ -2430,7 +2549,9 @@ export default function App() {
|
||||
const currentCookies = cookiesRef.current
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -2438,7 +2559,7 @@ export default function App() {
|
||||
const alreadyRunning = jobsRef.current.some((j) => {
|
||||
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
|
||||
|
||||
const jNorm0 = normalizeHttpUrl(String((j as any).sourceUrl || ''))
|
||||
@ -2447,7 +2568,7 @@ export default function App() {
|
||||
})
|
||||
if (alreadyRunning) return true
|
||||
|
||||
// ✅ Chaturbate-Startlogik mit Online-API:
|
||||
// Chaturbate-Startlogik mit Online-API:
|
||||
// public -> direkt starten
|
||||
// private/hidden/away -> wait_public
|
||||
// offline/unknown -> probe_retry mit hidden start
|
||||
@ -2472,7 +2593,10 @@ export default function App() {
|
||||
method: 'POST',
|
||||
cache: 'no-store',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ q: [mkLower] }),
|
||||
body: JSON.stringify({
|
||||
q: [mkLower],
|
||||
refresh: true,
|
||||
}),
|
||||
})
|
||||
|
||||
if (onlineRes.ok) {
|
||||
@ -2509,6 +2633,7 @@ export default function App() {
|
||||
// 2) in API + nicht public => warten bis public
|
||||
queuePendingAutoStart(mkLower, norm, resolvedShow, resolvedImageUrl, {
|
||||
mode: 'wait_public',
|
||||
source: 'manual',
|
||||
})
|
||||
return true
|
||||
} else {
|
||||
@ -2516,6 +2641,7 @@ export default function App() {
|
||||
queuePendingAutoStart(mkLower, norm, resolvedShow, resolvedImageUrl, {
|
||||
mode: 'probe_retry',
|
||||
nextProbeAtMs: Date.now() + 60_000,
|
||||
source: 'manual',
|
||||
})
|
||||
|
||||
const ok = await doStartNow(norm, silent, { hidden: true })
|
||||
@ -2532,6 +2658,7 @@ export default function App() {
|
||||
queuePendingAutoStart(mkLower, norm, 'unknown', undefined, {
|
||||
mode: 'probe_retry',
|
||||
nextProbeAtMs: Date.now() + 60_000,
|
||||
source: 'manual',
|
||||
})
|
||||
|
||||
const ok = await doStartNow(norm, silent, { hidden: true })
|
||||
@ -2552,23 +2679,36 @@ export default function App() {
|
||||
const created = await apiJSON<RecordJob>('/api/record', {
|
||||
method: 'POST',
|
||||
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.
|
||||
if (created?.id) startedToastByJobIdRef.current[String(created.id)] = true
|
||||
if ((createdWithModel as any)?.id) {
|
||||
startedToastByJobIdRef.current[String((createdWithModel as any).id)] = true
|
||||
}
|
||||
|
||||
setJobs((prev) => {
|
||||
const next = upsertJobById(prev, created)
|
||||
const next = upsertJobById(prev, createdWithModel)
|
||||
jobsRef.current = next
|
||||
return next
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (e: any) {
|
||||
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)) {
|
||||
showMissingCookiesMessage({ silent })
|
||||
return false
|
||||
|
||||
@ -31,7 +31,7 @@ export default function LastUpdatedText({ fetchedAt, enabled = true }: Props) {
|
||||
useEffect(() => {
|
||||
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)
|
||||
}, [enabled])
|
||||
|
||||
|
||||
@ -81,6 +81,35 @@ function regenerateTaskTitle(file: string) {
|
||||
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) {
|
||||
const [value, setValue] = useState<RecorderSettings>(DEFAULTS)
|
||||
const [saving, setSaving] = useState(false)
|
||||
@ -224,13 +253,16 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
if (!alive || !data) return
|
||||
|
||||
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) {
|
||||
setAssetsTask((t) => ({
|
||||
...t,
|
||||
status: 'running',
|
||||
title: 'Assets generieren',
|
||||
text: shortTaskFilename(String(assets.currentFile || assets.text || '')),
|
||||
text: assetsTaskText(assetsFile, assetsLabel, assetsPhase),
|
||||
done: Number(assets.done ?? 0),
|
||||
total: Number(assets.total ?? 0),
|
||||
err: undefined,
|
||||
@ -246,7 +278,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
assetsError.toLowerCase() === 'canceled'
|
||||
|
||||
setAssetsTask((t) => {
|
||||
// Wenn schon idle oder bereits am Ausfaden: nicht wieder "sichtbar reaktivieren"
|
||||
if (cancelled && (t.status === 'idle' || t.fading)) {
|
||||
return t
|
||||
}
|
||||
@ -255,7 +286,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
...t,
|
||||
status: cancelled ? 'cancelled' : 'error',
|
||||
title: 'Assets generieren',
|
||||
text: shortTaskFilename(String(assets.currentFile || assets.text || '')),
|
||||
text: assetsTaskText(assetsFile, assetsLabel, assetsPhase),
|
||||
done: Number(assets.done ?? 0),
|
||||
total: Number(assets.total ?? 0),
|
||||
err: cancelled ? undefined : assetsError,
|
||||
@ -270,25 +301,13 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
...t,
|
||||
status: 'done',
|
||||
title: 'Assets generieren',
|
||||
text: shortTaskFilename(String(assets.currentFile || assets.text || '')),
|
||||
text: assetsTaskText(assetsFile, assetsLabel, assetsPhase),
|
||||
done: Number(assets.done ?? 0),
|
||||
total: Number(assets.total ?? 0),
|
||||
err: undefined,
|
||||
cancellable: 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)
|
||||
@ -870,7 +889,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
<div className="mt-3 space-y-3">
|
||||
<Task
|
||||
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"
|
||||
startingLabel="Starte…"
|
||||
startUrl="/api/tasks/generate-assets"
|
||||
@ -891,13 +910,15 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
}))
|
||||
}}
|
||||
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) => ({
|
||||
...t,
|
||||
status: 'running',
|
||||
title: 'Assets generieren',
|
||||
text: fn || '',
|
||||
text: assetsTaskText(currentFile, currentLabel, currentPhase),
|
||||
done: p.done,
|
||||
total: p.total,
|
||||
}))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user