updated sse
This commit is contained in:
parent
ff06d0d18b
commit
80b9f69597
@ -935,8 +935,6 @@ func ensureAssetsForVideoDetailed(ctx context.Context, videoPath string, sourceU
|
||||
fmt.Printf("⚠️ preview clips skipped (invalid/incomplete input): %s\n", videoPath)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("⚠️ preview clips:", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -300,8 +300,6 @@ func syncChaturbateRoomStateIntoModelStore(store *ModelStore, rooms []Chaturbate
|
||||
img,
|
||||
fetchedAt,
|
||||
)
|
||||
|
||||
publishJobUpsertsForModelKey(modelKey)
|
||||
}
|
||||
|
||||
// bekannte Chaturbate-Models, die NICHT im Online-Snapshot sind => offline setzen
|
||||
@ -330,33 +328,6 @@ func syncChaturbateRoomStateIntoModelStore(store *ModelStore, rooms []Chaturbate
|
||||
"",
|
||||
fetchedAt,
|
||||
)
|
||||
|
||||
publishJobUpsertsForModelKey(modelKey)
|
||||
}
|
||||
}
|
||||
|
||||
func publishJobUpsertsForModelKey(modelKey string) {
|
||||
modelKey = strings.ToLower(strings.TrimSpace(modelKey))
|
||||
if modelKey == "" {
|
||||
return
|
||||
}
|
||||
|
||||
jobsMu.Lock()
|
||||
list := make([]*RecordJob, 0, len(jobs))
|
||||
for _, j := range jobs {
|
||||
if j == nil || j.Hidden {
|
||||
continue
|
||||
}
|
||||
if sseModelEventNameForJob(j) != modelKey {
|
||||
continue
|
||||
}
|
||||
c := *j
|
||||
list = append(list, &c)
|
||||
}
|
||||
jobsMu.Unlock()
|
||||
|
||||
for _, j := range list {
|
||||
publishJobUpsert(j)
|
||||
}
|
||||
}
|
||||
|
||||
@ -736,7 +707,6 @@ func refreshRunningJobsHLS(userLower string, newHls string, cookie string, ua st
|
||||
}
|
||||
|
||||
toStop := make([]*RecordJob, 0, 4)
|
||||
changedJobs := make([]*RecordJob, 0, 4)
|
||||
|
||||
jobsMu.Lock()
|
||||
for _, j := range jobs {
|
||||
@ -759,20 +729,12 @@ func refreshRunningJobsHLS(userLower string, newHls string, cookie string, ua st
|
||||
j.PreviewStateMsg = ""
|
||||
toStop = append(toStop, j)
|
||||
}
|
||||
|
||||
if !j.Hidden {
|
||||
changedJobs = append(changedJobs, j)
|
||||
}
|
||||
}
|
||||
jobsMu.Unlock()
|
||||
|
||||
for _, j := range toStop {
|
||||
stopPreview(j)
|
||||
}
|
||||
|
||||
for _, j := range changedJobs {
|
||||
publishJobUpsert(j)
|
||||
}
|
||||
}
|
||||
|
||||
func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -86,7 +86,6 @@ func stopJobsInternal(list []*RecordJob) {
|
||||
}
|
||||
|
||||
pl := make([]payload, 0, len(list))
|
||||
visibleJobs := make([]*RecordJob, 0, len(list))
|
||||
|
||||
jobsMu.Lock()
|
||||
for _, job := range list {
|
||||
@ -97,17 +96,9 @@ func stopJobsInternal(list []*RecordJob) {
|
||||
job.Progress = 10
|
||||
pl = append(pl, payload{cmd: job.previewCmd, cancel: job.cancel})
|
||||
job.previewCmd = nil
|
||||
|
||||
if !job.Hidden {
|
||||
visibleJobs = append(visibleJobs, job)
|
||||
}
|
||||
}
|
||||
jobsMu.Unlock()
|
||||
|
||||
for _, job := range visibleJobs {
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
|
||||
for _, p := range pl {
|
||||
if p.cmd != nil && p.cmd.Process != nil {
|
||||
_ = p.cmd.Process.Kill()
|
||||
@ -116,10 +107,6 @@ func stopJobsInternal(list []*RecordJob) {
|
||||
p.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
for _, job := range visibleJobs {
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
}
|
||||
|
||||
func stopAllStoppableJobs() int {
|
||||
|
||||
@ -350,13 +350,8 @@ func startPreviewHLS(ctx context.Context, job *RecordJob, m3u8URL, previewDir, h
|
||||
job.PreviewState = ""
|
||||
job.PreviewStateAt = ""
|
||||
job.PreviewStateMsg = ""
|
||||
hidden := job.Hidden
|
||||
jobsMu.Unlock()
|
||||
|
||||
if !hidden {
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
|
||||
commonIn := []string{"-y"}
|
||||
if strings.TrimSpace(userAgent) != "" {
|
||||
commonIn = append(commonIn, "-user_agent", userAgent)
|
||||
@ -423,13 +418,8 @@ func startPreviewHLS(ctx context.Context, job *RecordJob, m3u8URL, previewDir, h
|
||||
job.PreviewStateMsg = st
|
||||
}
|
||||
}
|
||||
hidden := job.Hidden
|
||||
jobsMu.Unlock()
|
||||
|
||||
if !hidden {
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
|
||||
//fmt.Printf("⚠️ preview hq ffmpeg failed: %v (%s)\n", err, st)
|
||||
}
|
||||
|
||||
|
||||
@ -259,7 +259,6 @@ func publishJob(jobID string) bool {
|
||||
j.Hidden = false
|
||||
jobsMu.Unlock()
|
||||
|
||||
publishJobUpsert(j)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -383,12 +382,7 @@ func setJobPhase(job *RecordJob, phase string, progress int) {
|
||||
jobsMu.Lock()
|
||||
job.Phase = phase
|
||||
job.Progress = progress
|
||||
hidden := job.Hidden
|
||||
jobsMu.Unlock()
|
||||
|
||||
if !hidden {
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
}
|
||||
|
||||
func durationSecondsCached(ctx context.Context, path string) (float64, error) {
|
||||
@ -1282,8 +1276,6 @@ func renameJobsOutputBasename(oldFile, newFile string) {
|
||||
return
|
||||
}
|
||||
|
||||
changedJobs := make([]*RecordJob, 0, 4)
|
||||
|
||||
jobsMu.Lock()
|
||||
for _, j := range jobs {
|
||||
if j == nil {
|
||||
@ -1295,16 +1287,9 @@ func renameJobsOutputBasename(oldFile, newFile string) {
|
||||
}
|
||||
if filepath.Base(out) == oldFile {
|
||||
j.Output = filepath.Join(filepath.Dir(out), newFile)
|
||||
if !j.Hidden {
|
||||
changedJobs = append(changedJobs, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
jobsMu.Unlock()
|
||||
|
||||
for _, j := range changedJobs {
|
||||
publishJobUpsert(j)
|
||||
}
|
||||
}
|
||||
|
||||
// nimmt jetzt *HTTPClient entgegen
|
||||
|
||||
@ -418,10 +418,6 @@ func startPostWorkStatusRefresher() {
|
||||
}
|
||||
}
|
||||
jobsMu.Unlock()
|
||||
|
||||
for _, job := range changedJobs {
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@ -166,213 +166,6 @@ func recordPrepareSplit(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
type regenerateAssetsRequest struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
func publishFinishedPostworkPhase(
|
||||
file string,
|
||||
assetID string,
|
||||
queue string,
|
||||
phase string,
|
||||
state string,
|
||||
label string,
|
||||
) {
|
||||
ev := finishedPostworkEvent{
|
||||
Type: "finished_postwork",
|
||||
File: file,
|
||||
AssetID: assetID,
|
||||
Queue: queue,
|
||||
State: state,
|
||||
Phase: phase,
|
||||
Label: label,
|
||||
TS: time.Now().UnixMilli(),
|
||||
}
|
||||
if b, err := json.Marshal(ev); err == nil {
|
||||
publishSSE("finishedPostwork", b)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRegenerateAssets(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req regenerateAssetsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid json body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file := strings.TrimSpace(req.File)
|
||||
if file == "" {
|
||||
http.Error(w, "missing file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !isAllowedVideoExt(file) {
|
||||
http.Error(w, "nicht erlaubt", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
s := getSettings()
|
||||
doneAbs, err := resolvePathRelativeToApp(s.DoneDir)
|
||||
if err != nil {
|
||||
http.Error(w, "doneDir auflösung fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(doneAbs) == "" {
|
||||
http.Error(w, "doneDir ist leer", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
videoPath, _, fi, err := resolveDoneFileByName(doneAbs, file)
|
||||
if err != nil {
|
||||
http.Error(w, "datei nicht gefunden", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if fi == nil || fi.IsDir() || fi.Size() <= 0 {
|
||||
http.Error(w, "invalid output path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
setRegenerateAssetsTaskRunning(file, file)
|
||||
|
||||
go func(videoPath string, file string) {
|
||||
id := assetIDFromVideoPath(videoPath)
|
||||
|
||||
// 1) Vorher evtl. laufende enrich-Jobs abbrechen
|
||||
cancelDeferredEnrichForFile(file)
|
||||
|
||||
// ✅ alten Analyze-Status aktiv löschen
|
||||
publishFinishedPostworkPhase(file, id, "enrich", "analyze", "missing", "")
|
||||
|
||||
// 2) SOFORT running an UI senden
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "meta", "running", "Meta")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "teaser", "running", "Teaser")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "thumb", "running", "Vorschaubild")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "sprites", "running", "Sprites")
|
||||
|
||||
notifyDoneChanged()
|
||||
|
||||
// 3) Alte Assets löschen
|
||||
if id != "" {
|
||||
if assetDir, _, _, _, metaPath, err := assetPathsForID(id); err == nil {
|
||||
_ = os.Remove(filepath.Join(assetDir, "preview.jpg"))
|
||||
_ = os.Remove(filepath.Join(assetDir, "preview.mp4"))
|
||||
_ = os.Remove(filepath.Join(assetDir, "preview-sprite.jpg"))
|
||||
_ = os.Remove(metaPath)
|
||||
}
|
||||
}
|
||||
|
||||
assetWatchDone := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(300 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
sentMetaDone := false
|
||||
sentTeaserDone := false
|
||||
sentThumbDone := false
|
||||
sentSpritesDone := false
|
||||
|
||||
checkAndEmit := func() {
|
||||
truth := finishedPhaseTruthForID(id)
|
||||
|
||||
if truth.MetaReady && !sentMetaDone {
|
||||
sentMetaDone = true
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "meta", "done", "Meta")
|
||||
}
|
||||
if truth.TeaserReady && !sentTeaserDone {
|
||||
sentTeaserDone = true
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "teaser", "done", "Teaser")
|
||||
}
|
||||
if truth.ThumbReady && !sentThumbDone {
|
||||
sentThumbDone = true
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "thumb", "done", "Vorschaubild")
|
||||
}
|
||||
if truth.SpritesReady && !sentSpritesDone {
|
||||
sentSpritesDone = true
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "sprites", "done", "Sprites")
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-assetWatchDone:
|
||||
checkAndEmit()
|
||||
return
|
||||
case <-ticker.C:
|
||||
checkAndEmit()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 4) Regeneration
|
||||
_, errAssets := ensureAssetsForVideoWithProgressCtx(context.Background(), videoPath, "", nil)
|
||||
close(assetWatchDone)
|
||||
|
||||
if errAssets == nil {
|
||||
publishFinishedPostworkPhase(file, id, "enrich", "analyze", "running", "KI-Analyse")
|
||||
}
|
||||
|
||||
errAI := ensureDeferredAssetsAndAI(context.Background(), videoPath, "")
|
||||
|
||||
if errAssets == nil && errAI == nil {
|
||||
publishFinishedPostworkPhase(file, id, "enrich", "analyze", "done", "KI-Analyse")
|
||||
}
|
||||
|
||||
// 5) Ergebnis zurück an UI
|
||||
if errAssets != nil || errAI != nil {
|
||||
errMsg := ""
|
||||
if errAssets != nil {
|
||||
errMsg = errAssets.Error()
|
||||
} else {
|
||||
errMsg = errAI.Error()
|
||||
}
|
||||
|
||||
setRegenerateAssetsTaskError(file, errMsg)
|
||||
|
||||
errEv := finishedPostworkEvent{
|
||||
Type: "finished_postwork",
|
||||
File: file,
|
||||
AssetID: id,
|
||||
Queue: "enrich",
|
||||
State: "error",
|
||||
Phase: "assets",
|
||||
Label: "Fehler",
|
||||
TS: time.Now().UnixMilli(),
|
||||
}
|
||||
if b, err := json.Marshal(errEv); err == nil {
|
||||
publishSSE("finishedPostwork", b)
|
||||
}
|
||||
|
||||
notifyDoneChanged()
|
||||
|
||||
time.AfterFunc(5*time.Second, func() {
|
||||
clearRegenerateAssetsTaskState()
|
||||
})
|
||||
|
||||
fmt.Println("[regenerate-assets] failed:", file, errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
setRegenerateAssetsTaskDone(file, "Fertig")
|
||||
|
||||
notifyDoneChanged()
|
||||
|
||||
time.AfterFunc(3*time.Second, func() {
|
||||
clearRegenerateAssetsTaskState()
|
||||
})
|
||||
}(videoPath, file)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"file": file,
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeleteMany(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
|
||||
@ -97,7 +97,6 @@ func RecordStream(
|
||||
jobsMu.Lock()
|
||||
job.SizeBytes = written
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
|
||||
lastPush = now
|
||||
lastBytes = written
|
||||
|
||||
@ -379,7 +379,6 @@ func handleM3U8Mode(ctx context.Context, m3u8URL, outFile string, job *RecordJob
|
||||
jobsMu.Lock()
|
||||
job.SizeBytes = sz
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
last = sz
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,8 +266,6 @@ func startRecordingInternal(req RecordRequest) (*RecordJob, error) {
|
||||
if j.Hidden && !req.Hidden {
|
||||
j.Hidden = false
|
||||
jobsMu.Unlock()
|
||||
|
||||
publishJobUpsert(j)
|
||||
return j, nil
|
||||
}
|
||||
|
||||
@ -317,10 +315,6 @@ func startRecordingInternal(req RecordRequest) (*RecordJob, error) {
|
||||
jobs[jobID] = job
|
||||
jobsMu.Unlock()
|
||||
|
||||
if !job.Hidden {
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
|
||||
go runJob(ctx, job, req)
|
||||
return job, nil
|
||||
}
|
||||
@ -350,7 +344,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
}
|
||||
|
||||
setJobProgress(job, "recording", 0)
|
||||
publishJobUpsert(job)
|
||||
|
||||
switch provider {
|
||||
case "chaturbate":
|
||||
@ -383,7 +376,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Lock()
|
||||
job.Output = outPath
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
|
||||
err = RecordStream(ctx, hc, "https://chaturbate.com/", username, outPath, req.Cookie, job)
|
||||
@ -404,7 +396,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Lock()
|
||||
job.Output = outPath
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
|
||||
err = RecordStreamMFC(ctx, hc, username, outPath, job)
|
||||
|
||||
@ -436,7 +427,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
job.Phase = "postwork"
|
||||
out := strings.TrimSpace(job.Output)
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
|
||||
if out == "" {
|
||||
jobsMu.Lock()
|
||||
@ -448,7 +438,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Unlock()
|
||||
|
||||
publishJobRemove(job)
|
||||
notifyDoneChanged()
|
||||
return
|
||||
}
|
||||
|
||||
@ -464,7 +453,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Unlock()
|
||||
|
||||
publishJobRemove(job)
|
||||
notifyDoneChanged()
|
||||
|
||||
if shouldLogRecordInfo(req) {
|
||||
if serr != nil {
|
||||
@ -486,7 +474,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Lock()
|
||||
job.SizeBytes = fi.Size()
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
|
||||
s := getSettings()
|
||||
minMB := s.AutoDeleteSmallDownloadsBelowMB
|
||||
@ -508,7 +495,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Unlock()
|
||||
|
||||
publishJobRemove(job)
|
||||
notifyDoneChanged()
|
||||
|
||||
if shouldLogRecordInfo(req) {
|
||||
fmt.Println("🧹 auto-deleted before enqueue:", base, "(size: "+formatBytesSI(fi.Size())+", threshold: "+formatBytesSI(threshold)+")")
|
||||
@ -537,7 +523,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
job.PostWork = &s
|
||||
}
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
|
||||
okQueued := postWorkQ.Enqueue(PostWorkTask{
|
||||
Key: postKey,
|
||||
@ -550,7 +535,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Unlock()
|
||||
|
||||
setJobProgress(job, "postwork", 0)
|
||||
publishJobUpsert(job)
|
||||
|
||||
publishFinishedPostworkStatusForJob(job, finishedPostworkEvent{
|
||||
File: postFile,
|
||||
@ -577,7 +561,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Unlock()
|
||||
|
||||
publishJobRemove(job)
|
||||
notifyDoneChanged()
|
||||
|
||||
publishFinishedPostworkStatusForJob(job, finishedPostworkEvent{
|
||||
File: postFile,
|
||||
@ -596,7 +579,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Lock()
|
||||
job.PostWork = &st
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
|
||||
label := "Nachbearbeitung läuft…"
|
||||
switch strings.ToLower(strings.TrimSpace(phase)) {
|
||||
@ -634,7 +616,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Lock()
|
||||
job.Output = out
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
}
|
||||
|
||||
@ -653,7 +634,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Unlock()
|
||||
|
||||
publishJobRemove(job)
|
||||
notifyDoneChanged()
|
||||
|
||||
publishFinishedPostworkStatusForJob(job, finishedPostworkEvent{
|
||||
File: filepath.Base(out),
|
||||
@ -682,7 +662,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Lock()
|
||||
job.Output = out
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
notifyDoneChanged()
|
||||
}
|
||||
|
||||
@ -694,7 +673,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Lock()
|
||||
job.DurationSeconds = sec
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
@ -711,7 +689,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
job.VideoHeight = h
|
||||
job.FPS = fps
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
}
|
||||
}
|
||||
|
||||
@ -767,7 +744,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Unlock()
|
||||
|
||||
publishJobRemove(job)
|
||||
notifyDoneChanged()
|
||||
|
||||
publishFinishedPostworkStatusForJob(job, finishedPostworkEvent{
|
||||
File: finalFile,
|
||||
@ -799,7 +775,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Lock()
|
||||
job.PostWork = &st
|
||||
jobsMu.Unlock()
|
||||
publishJobUpsert(job)
|
||||
|
||||
publishFinishedPostworkStatusForJob(job, finishedPostworkEvent{
|
||||
File: postFile,
|
||||
@ -823,7 +798,6 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) {
|
||||
jobsMu.Unlock()
|
||||
|
||||
publishJobRemove(job)
|
||||
notifyDoneChanged()
|
||||
|
||||
publishFinishedPostworkStatusForJob(job, finishedPostworkEvent{
|
||||
File: postFile,
|
||||
|
||||
@ -8,12 +8,12 @@
|
||||
"autoStartAddedDownloads": false,
|
||||
"useChaturbateApi": true,
|
||||
"useMyFreeCamsWatcher": true,
|
||||
"autoDeleteSmallDownloads": false,
|
||||
"autoDeleteSmallDownloads": true,
|
||||
"autoDeleteSmallDownloadsBelowMB": 200,
|
||||
"lowDiskPauseBelowGB": 5,
|
||||
"blurPreviews": false,
|
||||
"teaserPlayback": "hover",
|
||||
"teaserAudio": true,
|
||||
"enableNotifications": true,
|
||||
"encryptedCookies": "bs6T0hwuivNal4zH7v+yRK63wstZFvAnwGt1GEFDS2hMS4yWHXC/m2I0CnHtwgl7FS5ARSnY45tkMn9GJznNPUofuq2GDbHqZTwbRF3vNO/f/fnO9za4BQ+woU9303Act9U/UAQYi515LtRzTNdIczz1lkbahevMdyf0RLx7ajBgFmJ6UTbNtClwDRaLgdHxPWo8XMZRX9K0J1nWoJOAlvCWm60ggu+8UwnIEyPWDR15+kbFj1FjlwUOz8G/uPyJXnDYVA1fLpHSgUJHgarWMeTZNqDkONl6v6T7ksvp8PsWk/dmcrEz/vmEP8R78kLBpdgGPXvGfLo1sXfdt0MEvcfXm3RirzpsvpibJTlDUZv4eDth1kfppU3uEVH6TcQuwQ/0u3y1uecKRo9m9u40xAriM1ASFt4NUTLcCH5z6SgXxf0DT8jYZpIXWY/ma69aYW3laIuNx3+1Ybg1BUEJkdX+lFJieLrfWUwStO34bvRASapwXmSekvKpZe/53P91R4TKjbhBs0M="
|
||||
"encryptedCookies": "khmON0nSN9PsCC7Fb8btmhpmC9GmuQfWglOutI11SwrgM+EVs4SwkmcdOkPykPoWM+6oMqCYUCrCYF0ZHH18gVyZdiDC6GEpPhUQkjVYZJ8aOs4Gz0PSPQj89cMrQ9OBWVakaXAyFk1/m2xjez1ofA2uWlDREcSHPXhh24Gw9nICx23TkoKt5GJdy/+MH4qWI+qGI5zaYvr6dEO9iq4yDCpVgXeKPsylyRonDS0NuqYthjbay5P0A4Ir4Xr/66d4fp1+YLrEiWyASOsYvEmBhs9WO6/zNH/Uj4ZJqBlHaoCv1doaRBUy3XizWwh0Q71fGuGPrlqm8jjJBee3hbrgoC/22Ao6gC8+mLoG6ruqZNFjSecVSuDl/XTcIYgXqudUScLCemF40UT5rbs0aFH3awY4pm75K1AzL55lBHiSIb05UX/Rf1UUyP+ruFsGJBvHZkiOgAj8TpMoVNXpfTJQYDyZR6ISJHaFMfLZBUQ2U9Dim59Aon0nf6f84gsUhX2j3UsulkbtJxU="
|
||||
}
|
||||
|
||||
@ -70,6 +70,15 @@ type finishedPostworkEvent struct {
|
||||
TS int64 `json:"ts"`
|
||||
}
|
||||
|
||||
type taskStateEvent struct {
|
||||
Type string `json:"type"` // "task_state"
|
||||
GenerateAssetsRunning bool `json:"generateAssetsRunning"`
|
||||
RegenerateAssetsRunning bool `json:"regenerateAssetsRunning"`
|
||||
CleanupRunning bool `json:"cleanupRunning"`
|
||||
AnyRunning bool `json:"anyRunning"`
|
||||
TS int64 `json:"ts"`
|
||||
}
|
||||
|
||||
func publishFinishedPostworkStateForJob(
|
||||
j *RecordJob,
|
||||
queue string,
|
||||
@ -106,6 +115,24 @@ func publishFinishedPostworkStateForJob(
|
||||
})
|
||||
}
|
||||
|
||||
func publishTaskState() {
|
||||
assets := getGenerateAssetsTaskStatus()
|
||||
regenerate := snapshotRegenerateAssetsTaskState()
|
||||
cleanup := snapshotCleanupTaskState()
|
||||
|
||||
ev := taskStateEvent{
|
||||
Type: "task_state",
|
||||
GenerateAssetsRunning: assets.Running,
|
||||
RegenerateAssetsRunning: regenerate.Running,
|
||||
CleanupRunning: cleanup.Running,
|
||||
AnyRunning: assets.Running || regenerate.Running || cleanup.Running,
|
||||
TS: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(ev)
|
||||
publishSSE("taskState", b)
|
||||
}
|
||||
|
||||
func publishFinishedPostworkStatusForJob(j *RecordJob, ev finishedPostworkEvent) {
|
||||
if j == nil {
|
||||
return
|
||||
@ -125,10 +152,7 @@ func publishFinishedPostworkStatusForJob(j *RecordJob, ev finishedPostworkEvent)
|
||||
|
||||
b, _ := json.Marshal(ev)
|
||||
|
||||
// 1) modellbezogen für Running/Downloads
|
||||
publishSSE(eventName, b)
|
||||
|
||||
// 2) global für FinishedDownloads
|
||||
// nur global für FinishedDownloads
|
||||
publishSSE("finishedPostwork", b)
|
||||
}
|
||||
|
||||
@ -269,54 +293,6 @@ func visibleJobEventsJSON() []ssePublishItem {
|
||||
return out
|
||||
}
|
||||
|
||||
func publishJobUpsert(j *RecordJob) {
|
||||
if j == nil || j.Hidden {
|
||||
return
|
||||
}
|
||||
if !shouldPublishModelEventForJob(j) {
|
||||
return
|
||||
}
|
||||
|
||||
eventName := sseModelEventNameForJob(j)
|
||||
if eventName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
payload := jobEvent{
|
||||
Type: "job_upsert",
|
||||
Model: eventName,
|
||||
JobID: j.ID,
|
||||
Status: j.Status,
|
||||
Phase: j.Phase,
|
||||
Progress: j.Progress,
|
||||
SourceURL: j.SourceURL,
|
||||
Output: j.Output,
|
||||
StartedAt: j.StartedAt.Format(time.RFC3339Nano),
|
||||
StartedAtMs: j.StartedAtMs,
|
||||
EndedAtMs: j.EndedAtMs,
|
||||
SizeBytes: j.SizeBytes,
|
||||
DurationSeconds: j.DurationSeconds,
|
||||
PreviewState: j.PreviewState,
|
||||
PostWorkKey: strings.TrimSpace(j.PostWorkKey),
|
||||
PostWork: j.PostWork,
|
||||
TS: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
if sm := sseStoredModelForJob(j); sm != nil {
|
||||
payload.RoomStatus = strings.ToLower(strings.TrimSpace(sm.RoomStatus))
|
||||
payload.IsOnline = sm.IsOnline
|
||||
payload.ModelImageURL = strings.TrimSpace(sm.ImageURL)
|
||||
payload.ModelChatRoomURL = strings.TrimSpace(sm.ChatRoomURL)
|
||||
}
|
||||
|
||||
if j.EndedAt != nil {
|
||||
payload.EndedAt = j.EndedAt.Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(payload)
|
||||
publishSSE(eventName, b)
|
||||
}
|
||||
|
||||
func publishJobRemove(j *RecordJob) {
|
||||
if j == nil || j.Hidden {
|
||||
return
|
||||
|
||||
@ -46,6 +46,7 @@ func updateAssetsState(fn func(st *AssetsTaskState)) AssetsTaskState {
|
||||
assetsTaskMu.Unlock()
|
||||
|
||||
notifyAssetsChanged()
|
||||
publishTaskState()
|
||||
return st
|
||||
}
|
||||
|
||||
@ -100,8 +101,8 @@ func tasksGenerateAssets(w http.ResponseWriter, r *http.Request) {
|
||||
st := assetsTaskState
|
||||
assetsTaskMu.Unlock()
|
||||
|
||||
// SSE: Start pushen
|
||||
notifyAssetsChanged()
|
||||
publishTaskState()
|
||||
|
||||
go runGenerateMissingAssets(ctx)
|
||||
|
||||
@ -126,7 +127,7 @@ func tasksGenerateAssets(w http.ResponseWriter, r *http.Request) {
|
||||
// das macht der Worker zuverlässig im finishWithErr(context.Canceled))
|
||||
st := updateAssetsState(func(st *AssetsTaskState) {
|
||||
if st.Running {
|
||||
st.Error = "abgebrochen"
|
||||
st.Error = "Abgebrochen"
|
||||
}
|
||||
})
|
||||
|
||||
@ -200,7 +201,7 @@ func runGenerateMissingAssets(ctx context.Context) {
|
||||
}
|
||||
|
||||
if errors.Is(err, context.Canceled) {
|
||||
st.Error = "abgebrochen"
|
||||
st.Error = "Abgebrochen"
|
||||
} else {
|
||||
st.Error = err.Error()
|
||||
}
|
||||
|
||||
@ -40,6 +40,14 @@ type cleanupReq struct {
|
||||
BelowMB *int `json:"belowMB,omitempty"`
|
||||
}
|
||||
|
||||
func clearCleanupTaskState() {
|
||||
cleanupTaskMu.Lock()
|
||||
cleanupTaskState = CleanupTaskState{}
|
||||
cleanupTaskMu.Unlock()
|
||||
|
||||
publishTaskState()
|
||||
}
|
||||
|
||||
// /api/settings/cleanup (POST)
|
||||
// - löscht kleine Dateien < threshold MB (mp4/ts; skip .part/.tmp; skip keep-Ordner)
|
||||
// - räumt Orphans (preview/thumbs + generated) auf
|
||||
@ -55,6 +63,11 @@ func settingsCleanupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
doneAbs, err := resolvePathRelativeToApp(s.DoneDir)
|
||||
if err != nil || strings.TrimSpace(doneAbs) == "" {
|
||||
setCleanupTaskError("doneDir auflösung fehlgeschlagen")
|
||||
|
||||
time.AfterFunc(7*time.Second, func() {
|
||||
clearCleanupTaskState()
|
||||
})
|
||||
|
||||
http.Error(w, "doneDir auflösung fehlgeschlagen", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@ -96,6 +109,10 @@ func settingsCleanupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
orphansTotalRemoved := resp.GeneratedOrphansRemoved
|
||||
setCleanupTaskDone(fmt.Sprintf("geprüft: %d · Orphans: %d", resp.ScannedFiles, orphansTotalRemoved))
|
||||
|
||||
time.AfterFunc(7*time.Second, func() {
|
||||
clearCleanupTaskState()
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
|
||||
314
backend/tasks_regenerate_assets.go
Normal file
314
backend/tasks_regenerate_assets.go
Normal file
@ -0,0 +1,314 @@
|
||||
// backend\tasks_regenerate_assets.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type regenerateAssetsRequest struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
func publishFinishedPostworkPhase(
|
||||
file string,
|
||||
assetID string,
|
||||
queue string,
|
||||
phase string,
|
||||
state string,
|
||||
label string,
|
||||
) {
|
||||
ev := finishedPostworkEvent{
|
||||
Type: "finished_postwork",
|
||||
File: file,
|
||||
AssetID: assetID,
|
||||
Queue: queue,
|
||||
State: state,
|
||||
Phase: phase,
|
||||
Label: label,
|
||||
TS: time.Now().UnixMilli(),
|
||||
}
|
||||
if b, err := json.Marshal(ev); err == nil {
|
||||
publishSSE("finishedPostwork", b)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
regenerateAssetsRunMu sync.Mutex
|
||||
regenerateAssetsCancel context.CancelFunc
|
||||
regenerateAssetsFile string
|
||||
)
|
||||
|
||||
func handleRegenerateAssets(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost && r.Method != http.MethodDelete {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodDelete {
|
||||
regenerateAssetsRunMu.Lock()
|
||||
cancel := regenerateAssetsCancel
|
||||
file := regenerateAssetsFile
|
||||
regenerateAssetsCancel = nil
|
||||
regenerateAssetsFile = ""
|
||||
regenerateAssetsRunMu.Unlock()
|
||||
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
|
||||
if strings.TrimSpace(file) != "" {
|
||||
setRegenerateAssetsTaskError(file, "Abgebrochen")
|
||||
|
||||
id := strings.TrimSpace(assetIDFromVideoPath(file))
|
||||
if id == "" {
|
||||
id = stripHotPrefix(strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)))
|
||||
}
|
||||
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "meta", "missing", "")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "teaser", "missing", "")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "thumb", "missing", "")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "sprites", "missing", "")
|
||||
publishFinishedPostworkPhase(file, id, "enrich", "analyze", "missing", "")
|
||||
|
||||
time.AfterFunc(2*time.Second, func() {
|
||||
clearRegenerateAssetsTaskState()
|
||||
})
|
||||
} else {
|
||||
clearRegenerateAssetsTaskState()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"cancelled": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req regenerateAssetsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid json body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file := strings.TrimSpace(req.File)
|
||||
if file == "" {
|
||||
http.Error(w, "missing file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !isAllowedVideoExt(file) {
|
||||
http.Error(w, "nicht erlaubt", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
s := getSettings()
|
||||
doneAbs, err := resolvePathRelativeToApp(s.DoneDir)
|
||||
if err != nil {
|
||||
http.Error(w, "doneDir auflösung fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(doneAbs) == "" {
|
||||
http.Error(w, "doneDir ist leer", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
videoPath, _, fi, err := resolveDoneFileByName(doneAbs, file)
|
||||
if err != nil {
|
||||
http.Error(w, "datei nicht gefunden", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if fi == nil || fi.IsDir() || fi.Size() <= 0 {
|
||||
http.Error(w, "invalid output path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
runCtx, runCancel := context.WithCancel(context.Background())
|
||||
|
||||
regenerateAssetsRunMu.Lock()
|
||||
if regenerateAssetsCancel != nil {
|
||||
regenerateAssetsCancel()
|
||||
}
|
||||
regenerateAssetsCancel = runCancel
|
||||
regenerateAssetsFile = file
|
||||
regenerateAssetsRunMu.Unlock()
|
||||
|
||||
setRegenerateAssetsTaskRunning(file, file)
|
||||
|
||||
go func(ctx context.Context, videoPath string, file string) {
|
||||
defer func() {
|
||||
regenerateAssetsRunMu.Lock()
|
||||
if regenerateAssetsFile == file {
|
||||
regenerateAssetsCancel = nil
|
||||
regenerateAssetsFile = ""
|
||||
}
|
||||
regenerateAssetsRunMu.Unlock()
|
||||
}()
|
||||
|
||||
id := assetIDFromVideoPath(videoPath)
|
||||
if strings.TrimSpace(id) == "" {
|
||||
id = stripHotPrefix(strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)))
|
||||
}
|
||||
|
||||
cancelDeferredEnrichForFile(file)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
setRegenerateAssetsTaskError(file, "Abgebrochen")
|
||||
time.AfterFunc(2*time.Second, func() {
|
||||
clearRegenerateAssetsTaskState()
|
||||
})
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
publishFinishedPostworkPhase(file, id, "enrich", "analyze", "missing", "")
|
||||
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "meta", "running", "Meta")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "teaser", "running", "Teaser")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "thumb", "running", "Vorschaubild")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "sprites", "running", "Sprites")
|
||||
|
||||
if id != "" {
|
||||
if assetDir, _, _, _, metaPath, err := assetPathsForID(id); err == nil {
|
||||
_ = os.Remove(filepath.Join(assetDir, "preview.jpg"))
|
||||
_ = os.Remove(filepath.Join(assetDir, "preview.mp4"))
|
||||
_ = os.Remove(filepath.Join(assetDir, "preview-sprite.jpg"))
|
||||
_ = os.Remove(metaPath)
|
||||
}
|
||||
}
|
||||
|
||||
assetWatchDone := make(chan struct{})
|
||||
|
||||
go func(ctx context.Context, file string, id string) {
|
||||
ticker := time.NewTicker(300 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
sentMetaDone := false
|
||||
sentTeaserDone := false
|
||||
sentThumbDone := false
|
||||
sentSpritesDone := false
|
||||
|
||||
checkAndEmit := func() {
|
||||
truth := finishedPhaseTruthForID(id)
|
||||
|
||||
if truth.MetaReady && !sentMetaDone {
|
||||
sentMetaDone = true
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "meta", "done", "Meta")
|
||||
}
|
||||
if truth.TeaserReady && !sentTeaserDone {
|
||||
sentTeaserDone = true
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "teaser", "done", "Teaser")
|
||||
}
|
||||
if truth.ThumbReady && !sentThumbDone {
|
||||
sentThumbDone = true
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "thumb", "done", "Vorschaubild")
|
||||
}
|
||||
if truth.SpritesReady && !sentSpritesDone {
|
||||
sentSpritesDone = true
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "sprites", "done", "Sprites")
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-assetWatchDone:
|
||||
checkAndEmit()
|
||||
return
|
||||
case <-ticker.C:
|
||||
checkAndEmit()
|
||||
}
|
||||
}
|
||||
}(ctx, file, id)
|
||||
|
||||
_, errAssets := ensureAssetsForVideoWithProgressCtx(ctx, videoPath, "", nil)
|
||||
close(assetWatchDone)
|
||||
|
||||
if errors.Is(errAssets, context.Canceled) || ctx.Err() == context.Canceled {
|
||||
setRegenerateAssetsTaskError(file, "Abgebrochen")
|
||||
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "meta", "missing", "")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "teaser", "missing", "")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "thumb", "missing", "")
|
||||
publishFinishedPostworkPhase(file, id, "postwork", "sprites", "missing", "")
|
||||
publishFinishedPostworkPhase(file, id, "enrich", "analyze", "missing", "")
|
||||
|
||||
time.AfterFunc(2*time.Second, func() {
|
||||
clearRegenerateAssetsTaskState()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if errAssets == nil {
|
||||
publishFinishedPostworkPhase(file, id, "enrich", "analyze", "running", "KI-Analyse")
|
||||
}
|
||||
|
||||
errAI := ensureDeferredAssetsAndAI(ctx, videoPath, "")
|
||||
|
||||
if errors.Is(errAI, context.Canceled) || ctx.Err() == context.Canceled {
|
||||
setRegenerateAssetsTaskError(file, "Abgebrochen")
|
||||
publishFinishedPostworkPhase(file, id, "enrich", "analyze", "missing", "")
|
||||
|
||||
time.AfterFunc(2*time.Second, func() {
|
||||
clearRegenerateAssetsTaskState()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if errAssets == nil && errAI == nil {
|
||||
publishFinishedPostworkPhase(file, id, "enrich", "analyze", "done", "KI-Analyse")
|
||||
}
|
||||
|
||||
if errAssets != nil || errAI != nil {
|
||||
errMsg := ""
|
||||
if errAssets != nil {
|
||||
errMsg = errAssets.Error()
|
||||
} else {
|
||||
errMsg = errAI.Error()
|
||||
}
|
||||
|
||||
setRegenerateAssetsTaskError(file, errMsg)
|
||||
|
||||
errEv := finishedPostworkEvent{
|
||||
Type: "finished_postwork",
|
||||
File: file,
|
||||
AssetID: id,
|
||||
Queue: "enrich",
|
||||
State: "error",
|
||||
Phase: "assets",
|
||||
Label: "Fehler",
|
||||
TS: time.Now().UnixMilli(),
|
||||
}
|
||||
if b, err := json.Marshal(errEv); err == nil {
|
||||
publishSSE("finishedPostwork", b)
|
||||
}
|
||||
|
||||
time.AfterFunc(5*time.Second, func() {
|
||||
clearRegenerateAssetsTaskState()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setRegenerateAssetsTaskDone(file, "Fertig")
|
||||
|
||||
time.AfterFunc(3*time.Second, func() {
|
||||
clearRegenerateAssetsTaskState()
|
||||
})
|
||||
}(runCtx, videoPath, file)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"file": file,
|
||||
})
|
||||
}
|
||||
@ -63,6 +63,8 @@ func setRegenerateAssetsTaskRunning(file, text string) {
|
||||
Error: "",
|
||||
}
|
||||
regenerateAssetsTaskMu.Unlock()
|
||||
|
||||
publishTaskState()
|
||||
}
|
||||
|
||||
func setRegenerateAssetsTaskDone(file, text string) {
|
||||
@ -74,6 +76,8 @@ func setRegenerateAssetsTaskDone(file, text string) {
|
||||
Error: "",
|
||||
}
|
||||
regenerateAssetsTaskMu.Unlock()
|
||||
|
||||
publishTaskState()
|
||||
}
|
||||
|
||||
func setRegenerateAssetsTaskError(file, errText string) {
|
||||
@ -85,12 +89,16 @@ func setRegenerateAssetsTaskError(file, errText string) {
|
||||
Error: strings.TrimSpace(errText),
|
||||
}
|
||||
regenerateAssetsTaskMu.Unlock()
|
||||
|
||||
publishTaskState()
|
||||
}
|
||||
|
||||
func clearRegenerateAssetsTaskState() {
|
||||
regenerateAssetsTaskMu.Lock()
|
||||
regenerateAssetsTaskState = RegenerateAssetsTaskState{}
|
||||
regenerateAssetsTaskMu.Unlock()
|
||||
|
||||
publishTaskState()
|
||||
}
|
||||
|
||||
func snapshotCleanupTaskState() CleanupTaskState {
|
||||
@ -112,6 +120,8 @@ func setCleanupTaskRunning(text string) {
|
||||
FinishedAt: nil,
|
||||
}
|
||||
cleanupTaskMu.Unlock()
|
||||
|
||||
publishTaskState()
|
||||
}
|
||||
|
||||
func setCleanupTaskProgress(done, total int, currentFile, text string) {
|
||||
@ -124,6 +134,8 @@ func setCleanupTaskProgress(done, total int, currentFile, text string) {
|
||||
cleanupTaskState.Error = ""
|
||||
cleanupTaskState.FinishedAt = nil
|
||||
cleanupTaskMu.Unlock()
|
||||
|
||||
publishTaskState()
|
||||
}
|
||||
|
||||
func setCleanupTaskDone(text string) {
|
||||
@ -139,15 +151,20 @@ func setCleanupTaskDone(text string) {
|
||||
}
|
||||
|
||||
cleanupTaskMu.Unlock()
|
||||
|
||||
publishTaskState()
|
||||
}
|
||||
|
||||
func setCleanupTaskError(errText string) {
|
||||
cleanupTaskMu.Lock()
|
||||
cleanupTaskState.Running = false
|
||||
cleanupTaskState.Text = ""
|
||||
cleanupTaskState.Error = strings.TrimSpace(errText)
|
||||
cleanupTaskState.CurrentFile = ""
|
||||
cleanupTaskState.FinishedAt = cleanupNowISO()
|
||||
cleanupTaskMu.Unlock()
|
||||
|
||||
publishTaskState()
|
||||
}
|
||||
|
||||
func tasksStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -111,6 +111,15 @@ type JobEvent = {
|
||||
maxParallel?: number
|
||||
}
|
||||
|
||||
type TaskStateEvent = {
|
||||
type?: 'task_state'
|
||||
generateAssetsRunning?: boolean
|
||||
regenerateAssetsRunning?: boolean
|
||||
cleanupRunning?: boolean
|
||||
anyRunning?: boolean
|
||||
ts?: number
|
||||
}
|
||||
|
||||
const DEFAULT_RECORDER_SETTINGS: RecorderSettingsState = {
|
||||
recordDir: 'records',
|
||||
doneDir: 'records/done',
|
||||
@ -596,72 +605,6 @@ export default function App() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!authed) return
|
||||
|
||||
let cancelled = false
|
||||
let timer: number | null = null
|
||||
|
||||
const loadSettingsTaskState = async () => {
|
||||
try {
|
||||
const [genRes, statusRes] = await Promise.all([
|
||||
fetch('/api/tasks/generate-assets', { cache: 'no-store' as any }),
|
||||
fetch('/api/tasks/status', { cache: 'no-store' as any }),
|
||||
])
|
||||
|
||||
let generateAssetsRunning = false
|
||||
let regenerateAssetsRunning = false
|
||||
let cleanupRunning = false
|
||||
|
||||
if (genRes.ok) {
|
||||
const genData = await genRes.json().catch(() => null)
|
||||
generateAssetsRunning = Boolean(genData?.running)
|
||||
}
|
||||
|
||||
if (statusRes.ok) {
|
||||
const statusData = await statusRes.json().catch(() => null)
|
||||
regenerateAssetsRunning = Boolean(statusData?.regenerateAssets?.running)
|
||||
cleanupRunning = Boolean(statusData?.cleanup?.running)
|
||||
}
|
||||
|
||||
if (cancelled) return
|
||||
|
||||
setSettingsTaskRunning(
|
||||
generateAssetsRunning ||
|
||||
regenerateAssetsRunning ||
|
||||
cleanupRunning
|
||||
)
|
||||
} catch {
|
||||
if (!cancelled) setSettingsTaskRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const schedule = (ms: number) => {
|
||||
if (cancelled) return
|
||||
timer = window.setTimeout(async () => {
|
||||
await loadSettingsTaskState()
|
||||
schedule(document.hidden ? 8000 : 3000)
|
||||
}, ms)
|
||||
}
|
||||
|
||||
void loadSettingsTaskState()
|
||||
schedule(3000)
|
||||
|
||||
const onVis = () => {
|
||||
if (!document.hidden) {
|
||||
void loadSettingsTaskState()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', onVis)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (timer != null) window.clearTimeout(timer)
|
||||
document.removeEventListener('visibilitychange', onVis)
|
||||
}
|
||||
}, [authed])
|
||||
|
||||
useEffect(() => {
|
||||
void checkAuth()
|
||||
}, [checkAuth])
|
||||
@ -910,6 +853,10 @@ export default function App() {
|
||||
|
||||
const [modelsByKey, setModelsByKey] = useState<Record<string, StoredModel>>({})
|
||||
|
||||
useEffect(() => {
|
||||
setModelsCount(Object.keys(modelsByKey).length)
|
||||
}, [modelsByKey])
|
||||
|
||||
const isChaturbateStoreModel = useCallback((m?: StoredModel | null) => {
|
||||
const h = String(m?.host ?? '').toLowerCase()
|
||||
const input = String(m?.input ?? '').toLowerCase()
|
||||
@ -1166,7 +1113,9 @@ export default function App() {
|
||||
const refreshModelsByKey = useCallback(async () => {
|
||||
try {
|
||||
const list = await apiJSON<StoredModel[]>('/api/models', { cache: 'no-store' as any })
|
||||
setModelsByKey(buildModelsByKey(Array.isArray(list) ? list : []))
|
||||
const safeList = Array.isArray(list) ? list : []
|
||||
|
||||
setModelsByKey(buildModelsByKey(safeList))
|
||||
setLastHeaderUpdateAtMs(Date.now())
|
||||
} catch {
|
||||
// ignore
|
||||
@ -1411,7 +1360,9 @@ export default function App() {
|
||||
// ✅ 1) Update-Event mit Model: direkt in State übernehmen (KEIN /api/models)
|
||||
if (updated && typeof updated === 'object') {
|
||||
const k = String(updated.modelKey ?? '').toLowerCase().trim()
|
||||
if (k) setModelsByKey((prev) => ({ ...prev, [k]: updated }))
|
||||
if (k) {
|
||||
setModelsByKey((prev) => ({ ...prev, [k]: updated }))
|
||||
}
|
||||
|
||||
try {
|
||||
upsertModelCache(updated)
|
||||
@ -2173,31 +2124,6 @@ export default function App() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// ✅ Models-Count (leicht)
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const meta = await apiJSON<{ count?: number }>('/api/models/meta', { cache: 'no-store' })
|
||||
const c = Number(meta?.count ?? 0)
|
||||
if (!cancelled && Number.isFinite(c)) {
|
||||
setModelsCount(c)
|
||||
setLastHeaderUpdateAtMs(Date.now())
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
const t = window.setInterval(load, document.hidden ? 60000 : 30000)
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.clearInterval(t)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const initialCookies = useMemo(() => Object.entries(cookies).map(([name, value]) => ({ name, value })), [cookies])
|
||||
|
||||
const openPlayer = useCallback((job: RecordJob, startAtSec?: number) => {
|
||||
@ -2412,6 +2338,7 @@ export default function App() {
|
||||
|
||||
void loadDoneCount()
|
||||
void loadJobs()
|
||||
void loadTaskStateOnce()
|
||||
|
||||
if (selectedTabRef.current === 'finished') {
|
||||
requestFinishedReload('sse fallback poll')
|
||||
@ -2419,6 +2346,26 @@ export default function App() {
|
||||
}, document.hidden ? 60000 : 5000)
|
||||
}
|
||||
|
||||
const loadTaskStateOnce = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/tasks/status', { cache: 'no-store' as any })
|
||||
if (!res.ok) return
|
||||
|
||||
const data = await res.json().catch(() => null)
|
||||
|
||||
setSettingsTaskRunning(
|
||||
Boolean(
|
||||
data?.anyRunning ||
|
||||
data?.generateAssets?.running ||
|
||||
data?.regenerateAssets?.running ||
|
||||
data?.cleanup?.running
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const applyAutostartState = (data: unknown) => {
|
||||
const d = (data ?? {}) as any
|
||||
setAutostartState({
|
||||
@ -2483,8 +2430,23 @@ export default function App() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const onTaskState = (ev: MessageEvent) => {
|
||||
try {
|
||||
const msg = JSON.parse(String(ev.data ?? 'null')) as TaskStateEvent
|
||||
setSettingsTaskRunning(
|
||||
Boolean(
|
||||
msg?.anyRunning ||
|
||||
msg?.generateAssetsRunning ||
|
||||
msg?.regenerateAssetsRunning ||
|
||||
msg?.cleanupRunning
|
||||
)
|
||||
)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
void loadJobs()
|
||||
void loadDoneCount()
|
||||
void loadTaskStateOnce()
|
||||
|
||||
void apiJSON<AutostartState>('/api/autostart/state', { cache: 'no-store' as any })
|
||||
.then((s) => {
|
||||
@ -2502,6 +2464,7 @@ export default function App() {
|
||||
|
||||
es.onopen = () => {
|
||||
stopFallbackPoll()
|
||||
void loadTaskStateOnce()
|
||||
}
|
||||
|
||||
es.onerror = () => {
|
||||
@ -2511,11 +2474,14 @@ export default function App() {
|
||||
es.addEventListener('doneChanged', onDoneChanged as any)
|
||||
es.addEventListener('autostart', onAutostart as any)
|
||||
es.addEventListener('finishedPostwork', onFinishedPostwork as any)
|
||||
es.addEventListener('taskState', onTaskState as any)
|
||||
|
||||
const onVis = () => {
|
||||
if (document.hidden) return
|
||||
void loadJobs()
|
||||
void loadDoneCount()
|
||||
void loadTaskStateOnce()
|
||||
|
||||
if (selectedTabRef.current === 'finished') {
|
||||
requestFinishedReload('visibilitychange')
|
||||
}
|
||||
@ -2531,6 +2497,7 @@ export default function App() {
|
||||
es.removeEventListener('doneChanged', onDoneChanged as any)
|
||||
es.removeEventListener('autostart', onAutostart as any)
|
||||
es.removeEventListener('finishedPostwork', onFinishedPostwork as any)
|
||||
es.removeEventListener('taskState', onTaskState as any)
|
||||
|
||||
const handler = onModelJobEventRef.current
|
||||
if (handler) {
|
||||
@ -3550,7 +3517,6 @@ export default function App() {
|
||||
{selectedTab === 'settings' ? (
|
||||
<RecorderSettings
|
||||
onAssetsGenerated={bumpAssets}
|
||||
onTaskRunningChange={setSettingsTaskRunning}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
|
||||
@ -60,7 +60,6 @@ const DEFAULTS: RecorderSettings = {
|
||||
|
||||
type Props = {
|
||||
onAssetsGenerated?: () => void
|
||||
onTaskRunningChange?: (running: boolean) => void
|
||||
}
|
||||
|
||||
function shortTaskFilename(name?: string, max = 52) {
|
||||
@ -70,12 +69,11 @@ function shortTaskFilename(name?: string, max = 52) {
|
||||
return '…' + s.slice(-(max - 1))
|
||||
}
|
||||
|
||||
export default function RecorderSettings({ onAssetsGenerated, onTaskRunningChange }: Props) {
|
||||
export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
const [value, setValue] = useState<RecorderSettings>(DEFAULTS)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saveSuccessUntilMs, setSaveSuccessUntilMs] = useState<number>(0)
|
||||
const saveSuccessTimerRef = useRef<number | null>(null)
|
||||
const cleanupDismissUntilRef = useRef(0)
|
||||
const [cleaning, setCleaning] = useState(false)
|
||||
const [browsing, setBrowsing] = useState<'record' | 'done' | 'ffmpeg' | null>(null)
|
||||
const [msg, setMsg] = useState<string | null>(null)
|
||||
@ -152,16 +150,6 @@ export default function RecorderSettings({ onAssetsGenerated, onTaskRunningChang
|
||||
fading: false,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const running =
|
||||
assetsTask.status === 'running' ||
|
||||
regenerateAssetsTask.status === 'running' ||
|
||||
cleanupTask.status === 'running' ||
|
||||
cleaning
|
||||
|
||||
onTaskRunningChange?.(running)
|
||||
}, [assetsTask.status, regenerateAssetsTask.status, cleanupTask.status, cleaning, onTaskRunningChange])
|
||||
|
||||
const pauseGB = Number(value.lowDiskPauseBelowGB ?? DEFAULTS.lowDiskPauseBelowGB ?? 5)
|
||||
const uiPauseGB = diskStatus?.pauseGB ?? pauseGB
|
||||
const uiResumeGB = diskStatus?.resumeGB ?? (pauseGB + 3)
|
||||
@ -239,12 +227,19 @@ export default function RecorderSettings({ onAssetsGenerated, onTaskRunningChang
|
||||
fading: false,
|
||||
}))
|
||||
} else if (regen?.error) {
|
||||
const regenError = String(regen.error || '').trim()
|
||||
const cancelled =
|
||||
regenError.toLowerCase() === 'abgebrochen' ||
|
||||
regenError.toLowerCase() === 'abbruch' ||
|
||||
regenError.toLowerCase() === 'cancelled' ||
|
||||
regenError.toLowerCase() === 'canceled'
|
||||
|
||||
setRegenerateAssetsTask((t) => ({
|
||||
...t,
|
||||
status: 'error',
|
||||
status: cancelled ? 'cancelled' : 'error',
|
||||
title: 'Assets neu generieren',
|
||||
text: String(regen.file || ''),
|
||||
err: String(regen.error),
|
||||
text: String(regen.file || ''), // nur Dateiname anzeigen, kein "Abgebrochen" darunter
|
||||
err: cancelled ? undefined : regenError,
|
||||
cancellable: false,
|
||||
fading: false,
|
||||
}))
|
||||
@ -270,8 +265,8 @@ export default function RecorderSettings({ onAssetsGenerated, onTaskRunningChang
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
const cleanup = data.cleanup
|
||||
const cleanupSuppressed = Date.now() < cleanupDismissUntilRef.current
|
||||
|
||||
if (cleanup?.running) {
|
||||
setCleaning(true)
|
||||
@ -286,7 +281,7 @@ export default function RecorderSettings({ onAssetsGenerated, onTaskRunningChang
|
||||
cancellable: false,
|
||||
fading: false,
|
||||
}))
|
||||
} else if (!cleanupSuppressed && cleanup?.error) {
|
||||
} else if (cleanup?.error) {
|
||||
setCleaning(false)
|
||||
setCleanupTask((t) => ({
|
||||
...t,
|
||||
@ -299,7 +294,7 @@ export default function RecorderSettings({ onAssetsGenerated, onTaskRunningChang
|
||||
cancellable: false,
|
||||
fading: false,
|
||||
}))
|
||||
} else if (!cleanupSuppressed && (cleanup?.finishedAt || cleanup?.text)) {
|
||||
} else if (cleanup?.finishedAt || cleanup?.text) {
|
||||
setCleaning(false)
|
||||
setCleanupTask((t) => ({
|
||||
...t,
|
||||
@ -618,8 +613,7 @@ export default function RecorderSettings({ onAssetsGenerated, onTaskRunningChang
|
||||
text: `geprüft: ${scannedFiles} · Orphans: ${orphansTotalRemoved}`,
|
||||
}))
|
||||
|
||||
cleanupDismissUntilRef.current = Date.now() + 4500
|
||||
fadeOutTask(setCleanupTask) // ✅ nach und nach ausfaden
|
||||
fadeOutTask(setCleanupTask, 7000, 500)
|
||||
} catch (e: any) {
|
||||
const msg = e?.message ?? String(e)
|
||||
setErr(msg)
|
||||
@ -631,7 +625,7 @@ export default function RecorderSettings({ onAssetsGenerated, onTaskRunningChang
|
||||
err: msg,
|
||||
}))
|
||||
|
||||
fadeOutTask(setCleanupTask)
|
||||
fadeOutTask(setCleanupTask, 7000, 500)
|
||||
} finally {
|
||||
setCleaning(false)
|
||||
}
|
||||
@ -663,8 +657,9 @@ export default function RecorderSettings({ onAssetsGenerated, onTaskRunningChang
|
||||
...t,
|
||||
status: 'cancelled',
|
||||
title: 'Assets neu generieren',
|
||||
text: 'Abgebrochen.',
|
||||
text: t.text,
|
||||
err: undefined,
|
||||
cancellable: false,
|
||||
fading: false,
|
||||
}))
|
||||
|
||||
|
||||
@ -105,15 +105,15 @@ export default function TaskList({ tasks, onCancel }: Props) {
|
||||
{/* Status badge (running bewusst ohne Badge) */}
|
||||
{t.status === 'done' ? (
|
||||
<span className="shrink-0 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-[11px] font-semibold text-green-800 ring-1 ring-inset ring-green-200 dark:bg-green-500/20 dark:text-green-200 dark:ring-green-400/30">
|
||||
fertig
|
||||
Fertig
|
||||
</span>
|
||||
) : t.status === 'cancelled' ? (
|
||||
<span className="shrink-0 inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-semibold text-gray-700 ring-1 ring-inset ring-gray-200 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10">
|
||||
abgebrochen
|
||||
Abgebrochen
|
||||
</span>
|
||||
) : t.status === 'error' ? (
|
||||
<span className="shrink-0 inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-[11px] font-semibold text-red-800 ring-1 ring-inset ring-red-200 dark:bg-red-500/20 dark:text-red-200 dark:ring-red-400/30">
|
||||
fehler
|
||||
Fehler
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user