updated
This commit is contained in:
parent
013f616303
commit
12fc1e2f83
@ -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/<id>/ (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/<assetID>)
|
||||
_ = os.RemoveAll(filepath.Join(os.TempDir(), "rec_preview", id))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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/<id> 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/<id> prüfen
|
||||
metaRoot, err := generatedMetaRoot()
|
||||
if err == nil {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user