From 12fc1e2f833773ac5bca7287a121a3d8080494e4 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:38:25 +0100 Subject: [PATCH] updated --- backend/main.go | 45 +++++++++++++--- backend/preview.go | 2 +- backend/record_stream_cb.go | 36 ++++--------- backend/tasks_cleanup.go | 17 ++++++ frontend/src/App.tsx | 54 +++++++++++++++++-- .../src/components/ui/FinishedDownloads.tsx | 8 +++ 6 files changed, 124 insertions(+), 38 deletions(-) diff --git a/backend/main.go b/backend/main.go index 9ce907b..3e3dec8 100644 --- a/backend/main.go +++ b/backend/main.go @@ -505,10 +505,46 @@ func detectFFmpegPath() string { return "ffmpeg" } -func removeGeneratedForID(id string) { - // ✅ canonical id: wie beim Erzeugen der generated Ordner +func isAssetIDInUse(id string) bool { id = strings.TrimSpace(id) if id == "" { + return false + } + + jobsMu.Lock() + defer jobsMu.Unlock() + + for _, j := range jobs { + if j == nil { + continue + } + + aid := strings.TrimSpace(assetIDForJob(j)) + if aid == "" || !strings.EqualFold(aid, id) { + continue + } + + // Alles was noch nicht terminal ist, gilt als "in Benutzung" + if !isTerminalJobStatus(j.Status) { + return true + } + + // Auch Postwork/Queue lieber schützen + if isPostworkJob(j) { + return true + } + } + + return false +} + +func removeGeneratedForID(id string) { + id = strings.TrimSpace(id) + if id == "" { + return + } + + if isAssetIDInUse(id) { return } @@ -530,11 +566,6 @@ func removeGeneratedForID(id string) { _ = os.RemoveAll(filepath.Join(root, id)) } - // (optional aber sinnvoll) 1b) Legacy: generated// (falls noch alte Assets existieren) - if root, _ := generatedRoot(); strings.TrimSpace(root) != "" { - _ = os.RemoveAll(filepath.Join(root, id)) - } - // 2) Temp Preview Segmente (HLS) wegräumen // (%TEMP%/rec_preview/) _ = os.RemoveAll(filepath.Join(os.TempDir(), "rec_preview", id)) diff --git a/backend/preview.go b/backend/preview.go index 4d46d86..9c15574 100644 --- a/backend/preview.go +++ b/backend/preview.go @@ -1623,7 +1623,7 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri if previewDir != "" { img, genErr = extractLastFrameFromPreviewDirJPG(previewDir) if genErr != nil { - fmt.Println("preview from previewDir failed:", previewDir, genErr) + //fmt.Println("preview from previewDir failed:", previewDir, genErr) } } diff --git a/backend/record_stream_cb.go b/backend/record_stream_cb.go index 870585d..74bc7b4 100644 --- a/backend/record_stream_cb.go +++ b/backend/record_stream_cb.go @@ -79,11 +79,7 @@ func RecordStream( if sdir, serr := ensurePreviewSegmentsDir(previewDir); serr == nil { segmentsDir = strings.TrimSpace(sdir) - } else { - fmt.Println("preview ensurePreviewSegmentsDir failed:", assetID, serr) } - } else { - fmt.Println("preview ensureGeneratedDir failed:", assetID, derr) } } @@ -191,18 +187,18 @@ func RecordStream( } if segmentsDir != "" { - ext := ".m4s" - if len(b) > 0 && b[0] == 0x47 { - ext = ".ts" - } + if err := os.MkdirAll(segmentsDir, 0o755); err == nil { + ext := ".m4s" + if len(b) > 0 && b[0] == 0x47 { + ext = ".ts" + } - segName := fmt.Sprintf("seg_preview_%020d%s", time.Now().UnixNano(), ext) - segPath := filepath.Join(segmentsDir, segName) + segName := fmt.Sprintf("seg_preview_%020d%s", time.Now().UnixNano(), ext) + segPath := filepath.Join(segmentsDir, segName) - if err := os.WriteFile(segPath, b, 0o644); err != nil { - fmt.Println("preview segment write failed:", segPath, err) - } else { - trimPreviewSegments(segmentsDir, 12) + if err := os.WriteFile(segPath, b, 0o644); err == nil { + trimPreviewSegments(segmentsDir, 12) + } } } @@ -301,7 +297,6 @@ func (p *Playlist) WatchSegments( segDir, err := ensurePreviewSegmentsDir(previewDir) if err != nil { - fmt.Println("ensurePreviewSegmentsDir failed:", previewDir, err) return } initPath := filepath.Join(segDir, "init.mp4") @@ -315,35 +310,29 @@ func (p *Playlist) WatchSegments( req, err := hc.NewRequest(ctx, http.MethodGet, mapURL, httpCookie) if err != nil { - fmt.Println("init request build failed:", mapURL, err) return } resp, err := hc.client.Do(req) if err != nil { - fmt.Println("init download failed:", mapURL, err) return } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { - fmt.Println("init bad status:", resp.StatusCode, mapURL) _, _ = io.Copy(io.Discard, resp.Body) return } b, err := io.ReadAll(resp.Body) if err != nil { - fmt.Println("init read failed:", mapURL, err) return } if len(b) == 0 { - fmt.Println("init empty:", mapURL) return } if err := os.WriteFile(initPath, b, 0o644); err != nil { - fmt.Println("init write failed:", initPath, err) return } @@ -369,7 +358,7 @@ func (p *Playlist) WatchSegments( resp, err := hc.client.Do(req) if err != nil { emptyRounds++ - fmt.Println("playlist download failed:", p.PlaylistURL, err) + //fmt.Println("playlist download failed:", p.PlaylistURL, err) if emptyRounds >= maxEmptyRounds { return errors.New("❌ Playlist nicht mehr erreichbar – Stream vermutlich offline") @@ -405,7 +394,6 @@ func (p *Playlist) WatchSegments( resp.Body.Close() if err != nil { emptyRounds++ - fmt.Println("playlist decode failed:", p.PlaylistURL, err) if emptyRounds >= maxEmptyRounds { return errors.New("❌ Fehlerhafte Playlist – möglicherweise offline") @@ -421,7 +409,6 @@ func (p *Playlist) WatchSegments( if listType != m3u8.MEDIA { emptyRounds++ - fmt.Println("playlist is not media playlist:", p.PlaylistURL) if emptyRounds >= maxEmptyRounds { return errors.New("❌ Fehlerhafte Playlist – möglicherweise offline") @@ -438,7 +425,6 @@ func (p *Playlist) WatchSegments( media, ok := playlist.(*m3u8.MediaPlaylist) if !ok || media == nil { emptyRounds++ - fmt.Println("playlist cast to media failed:", p.PlaylistURL) if emptyRounds >= maxEmptyRounds { return errors.New("❌ Fehlerhafte Media-Playlist – möglicherweise offline") diff --git a/backend/tasks_cleanup.go b/backend/tasks_cleanup.go index 6d7b7eb..fa09aca 100644 --- a/backend/tasks_cleanup.go +++ b/backend/tasks_cleanup.go @@ -310,6 +310,23 @@ func runGeneratedGarbageCollector() generatedGCStats { return nil }) + // 1b) Auch aktuell laufende / nachbearbeitete Jobs als "live" behandeln. + // Sonst räumt der GC generated/meta/ weg, obwohl RecordStream dort noch schreibt. + jobsMu.Lock() + for _, j := range jobs { + if j == nil { + continue + } + + id := strings.TrimSpace(assetIDForJob(j)) + if id == "" { + continue + } + + live[id] = struct{}{} + } + jobsMu.Unlock() + // 2) /generated/meta/ prüfen metaRoot, err := generatedMetaRoot() if err == nil { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 320df57..5698bad 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2860,6 +2860,10 @@ export default function App() { // ✅ Prefetch wieder nachfüllen void prefetchDonePage(donePage + 1) + + // ✅ wichtig + requestFinishedReload('delete success') + void loadDoneCount() }, 320) }, onError: async () => { @@ -2888,16 +2892,48 @@ export default function App() { run: () => apiJSON(`/api/record/keep?file=${encodeURIComponent(file)}`, { method: 'POST' }), onSuccess: async () => { window.setTimeout(() => { - // ✅ Entfernt aus Finished + Running + Player - setDoneJobs((prev) => prev.filter((j) => baseName(j.output || '') !== file)) + // ✅ Done-Liste lokal bereinigen + Seite direkt wieder auffüllen (aus Prefetch) + setDoneJobs((prev) => { + const filtered = prev.filter((j) => baseName(j.output || '') !== file) + + const need = DONE_PAGE_SIZE - filtered.length + if (need <= 0) return filtered + + const prefetchKey = makePrefetchKey(donePage + 1, doneSort) + const buf = donePrefetchRef.current + + if (!buf || buf.key !== prefetchKey || !Array.isArray(buf.items) || buf.items.length === 0) { + return filtered + } + + const next: RecordJob[] = [...filtered] + const used = new Set(next.map((x) => String(x.id || baseName(x.output || '')).trim())) + + while (next.length < DONE_PAGE_SIZE && buf.items.length > 0) { + const cand = buf.items.shift()! + const id = String(cand.id || baseName(cand.output || '')).trim() + if (!id || used.has(id)) continue + used.add(id) + next.push(cand) + } + + donePrefetchRef.current = { ...buf, items: buf.items, ts: buf.ts } + return next + }) + + // ✅ Entfernt aus Running + Player setJobs((prev) => prev.filter((j) => baseName(j.output || '') !== file)) setPlayerJob((prev) => (prev && baseName(prev.output || '') === file ? null : prev)) - // ✅ Count ebenfalls runter (Keep entfernt den Eintrag aus "finished") + // ✅ Count runter setDoneCount((c) => Math.max(0, c - 1)) - // Optional: nächste Seite vorladen (schadet nicht) + // ✅ Prefetch wieder nachfüllen void prefetchDonePage(donePage + 1) + + // ✅ Finished-Liste aktiv neu laden + requestFinishedReload('keep success') + void loadDoneCount() }, 320) }, onError: async () => { @@ -2908,7 +2944,15 @@ export default function App() { return } }, - [runFinishedFileAction, notify, donePage, prefetchDonePage] + [ + runFinishedFileAction, + notify, + donePage, + doneSort, + prefetchDonePage, + requestFinishedReload, + loadDoneCount, + ] ) const handleToggleHot = useCallback( diff --git a/frontend/src/components/ui/FinishedDownloads.tsx b/frontend/src/components/ui/FinishedDownloads.tsx index 381ad8f..73b1ddc 100644 --- a/frontend/src/components/ui/FinishedDownloads.tsx +++ b/frontend/src/components/ui/FinishedDownloads.tsx @@ -3126,6 +3126,14 @@ export default function FinishedDownloads({ if (detail.phase === 'success') { markDeleting(key, false) + + // ✅ Nach erfolgreichem externen Delete aktuelle Seite nachziehen + if (refillInFlightRef.current) { + refillQueuedWhileInFlightRef.current = true + } else { + queueRefill() + } + return } }