updated
This commit is contained in:
parent
013f616303
commit
12fc1e2f83
@ -505,10 +505,46 @@ func detectFFmpegPath() string {
|
|||||||
return "ffmpeg"
|
return "ffmpeg"
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeGeneratedForID(id string) {
|
func isAssetIDInUse(id string) bool {
|
||||||
// ✅ canonical id: wie beim Erzeugen der generated Ordner
|
|
||||||
id = strings.TrimSpace(id)
|
id = strings.TrimSpace(id)
|
||||||
if 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -530,11 +566,6 @@ func removeGeneratedForID(id string) {
|
|||||||
_ = os.RemoveAll(filepath.Join(root, id))
|
_ = 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
|
// 2) Temp Preview Segmente (HLS) wegräumen
|
||||||
// (%TEMP%/rec_preview/<assetID>)
|
// (%TEMP%/rec_preview/<assetID>)
|
||||||
_ = os.RemoveAll(filepath.Join(os.TempDir(), "rec_preview", id))
|
_ = 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 != "" {
|
if previewDir != "" {
|
||||||
img, genErr = extractLastFrameFromPreviewDirJPG(previewDir)
|
img, genErr = extractLastFrameFromPreviewDirJPG(previewDir)
|
||||||
if genErr != nil {
|
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 {
|
if sdir, serr := ensurePreviewSegmentsDir(previewDir); serr == nil {
|
||||||
segmentsDir = strings.TrimSpace(sdir)
|
segmentsDir = strings.TrimSpace(sdir)
|
||||||
} else {
|
|
||||||
fmt.Println("preview ensurePreviewSegmentsDir failed:", assetID, serr)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
fmt.Println("preview ensureGeneratedDir failed:", assetID, derr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +187,7 @@ func RecordStream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if segmentsDir != "" {
|
if segmentsDir != "" {
|
||||||
|
if err := os.MkdirAll(segmentsDir, 0o755); err == nil {
|
||||||
ext := ".m4s"
|
ext := ".m4s"
|
||||||
if len(b) > 0 && b[0] == 0x47 {
|
if len(b) > 0 && b[0] == 0x47 {
|
||||||
ext = ".ts"
|
ext = ".ts"
|
||||||
@ -199,12 +196,11 @@ func RecordStream(
|
|||||||
segName := fmt.Sprintf("seg_preview_%020d%s", time.Now().UnixNano(), ext)
|
segName := fmt.Sprintf("seg_preview_%020d%s", time.Now().UnixNano(), ext)
|
||||||
segPath := filepath.Join(segmentsDir, segName)
|
segPath := filepath.Join(segmentsDir, segName)
|
||||||
|
|
||||||
if err := os.WriteFile(segPath, b, 0o644); err != nil {
|
if err := os.WriteFile(segPath, b, 0o644); err == nil {
|
||||||
fmt.Println("preview segment write failed:", segPath, err)
|
|
||||||
} else {
|
|
||||||
trimPreviewSegments(segmentsDir, 12)
|
trimPreviewSegments(segmentsDir, 12)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if job != nil && !published {
|
if job != nil && !published {
|
||||||
published = true
|
published = true
|
||||||
@ -301,7 +297,6 @@ func (p *Playlist) WatchSegments(
|
|||||||
|
|
||||||
segDir, err := ensurePreviewSegmentsDir(previewDir)
|
segDir, err := ensurePreviewSegmentsDir(previewDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("ensurePreviewSegmentsDir failed:", previewDir, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
initPath := filepath.Join(segDir, "init.mp4")
|
initPath := filepath.Join(segDir, "init.mp4")
|
||||||
@ -315,35 +310,29 @@ func (p *Playlist) WatchSegments(
|
|||||||
|
|
||||||
req, err := hc.NewRequest(ctx, http.MethodGet, mapURL, httpCookie)
|
req, err := hc.NewRequest(ctx, http.MethodGet, mapURL, httpCookie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("init request build failed:", mapURL, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := hc.client.Do(req)
|
resp, err := hc.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("init download failed:", mapURL, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
fmt.Println("init bad status:", resp.StatusCode, mapURL)
|
|
||||||
_, _ = io.Copy(io.Discard, resp.Body)
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := io.ReadAll(resp.Body)
|
b, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("init read failed:", mapURL, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(b) == 0 {
|
if len(b) == 0 {
|
||||||
fmt.Println("init empty:", mapURL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(initPath, b, 0o644); err != nil {
|
if err := os.WriteFile(initPath, b, 0o644); err != nil {
|
||||||
fmt.Println("init write failed:", initPath, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,7 +358,7 @@ func (p *Playlist) WatchSegments(
|
|||||||
resp, err := hc.client.Do(req)
|
resp, err := hc.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
emptyRounds++
|
emptyRounds++
|
||||||
fmt.Println("playlist download failed:", p.PlaylistURL, err)
|
//fmt.Println("playlist download failed:", p.PlaylistURL, err)
|
||||||
|
|
||||||
if emptyRounds >= maxEmptyRounds {
|
if emptyRounds >= maxEmptyRounds {
|
||||||
return errors.New("❌ Playlist nicht mehr erreichbar – Stream vermutlich offline")
|
return errors.New("❌ Playlist nicht mehr erreichbar – Stream vermutlich offline")
|
||||||
@ -405,7 +394,6 @@ func (p *Playlist) WatchSegments(
|
|||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
emptyRounds++
|
emptyRounds++
|
||||||
fmt.Println("playlist decode failed:", p.PlaylistURL, err)
|
|
||||||
|
|
||||||
if emptyRounds >= maxEmptyRounds {
|
if emptyRounds >= maxEmptyRounds {
|
||||||
return errors.New("❌ Fehlerhafte Playlist – möglicherweise offline")
|
return errors.New("❌ Fehlerhafte Playlist – möglicherweise offline")
|
||||||
@ -421,7 +409,6 @@ func (p *Playlist) WatchSegments(
|
|||||||
|
|
||||||
if listType != m3u8.MEDIA {
|
if listType != m3u8.MEDIA {
|
||||||
emptyRounds++
|
emptyRounds++
|
||||||
fmt.Println("playlist is not media playlist:", p.PlaylistURL)
|
|
||||||
|
|
||||||
if emptyRounds >= maxEmptyRounds {
|
if emptyRounds >= maxEmptyRounds {
|
||||||
return errors.New("❌ Fehlerhafte Playlist – möglicherweise offline")
|
return errors.New("❌ Fehlerhafte Playlist – möglicherweise offline")
|
||||||
@ -438,7 +425,6 @@ func (p *Playlist) WatchSegments(
|
|||||||
media, ok := playlist.(*m3u8.MediaPlaylist)
|
media, ok := playlist.(*m3u8.MediaPlaylist)
|
||||||
if !ok || media == nil {
|
if !ok || media == nil {
|
||||||
emptyRounds++
|
emptyRounds++
|
||||||
fmt.Println("playlist cast to media failed:", p.PlaylistURL)
|
|
||||||
|
|
||||||
if emptyRounds >= maxEmptyRounds {
|
if emptyRounds >= maxEmptyRounds {
|
||||||
return errors.New("❌ Fehlerhafte Media-Playlist – möglicherweise offline")
|
return errors.New("❌ Fehlerhafte Media-Playlist – möglicherweise offline")
|
||||||
|
|||||||
@ -310,6 +310,23 @@ func runGeneratedGarbageCollector() generatedGCStats {
|
|||||||
return nil
|
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
|
// 2) /generated/meta/<id> prüfen
|
||||||
metaRoot, err := generatedMetaRoot()
|
metaRoot, err := generatedMetaRoot()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@ -2860,6 +2860,10 @@ export default function App() {
|
|||||||
|
|
||||||
// ✅ Prefetch wieder nachfüllen
|
// ✅ Prefetch wieder nachfüllen
|
||||||
void prefetchDonePage(donePage + 1)
|
void prefetchDonePage(donePage + 1)
|
||||||
|
|
||||||
|
// ✅ wichtig
|
||||||
|
requestFinishedReload('delete success')
|
||||||
|
void loadDoneCount()
|
||||||
}, 320)
|
}, 320)
|
||||||
},
|
},
|
||||||
onError: async () => {
|
onError: async () => {
|
||||||
@ -2888,16 +2892,48 @@ export default function App() {
|
|||||||
run: () => apiJSON(`/api/record/keep?file=${encodeURIComponent(file)}`, { method: 'POST' }),
|
run: () => apiJSON(`/api/record/keep?file=${encodeURIComponent(file)}`, { method: 'POST' }),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
// ✅ Entfernt aus Finished + Running + Player
|
// ✅ Done-Liste lokal bereinigen + Seite direkt wieder auffüllen (aus Prefetch)
|
||||||
setDoneJobs((prev) => prev.filter((j) => baseName(j.output || '') !== file))
|
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))
|
setJobs((prev) => prev.filter((j) => baseName(j.output || '') !== file))
|
||||||
setPlayerJob((prev) => (prev && baseName(prev.output || '') === file ? null : prev))
|
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))
|
setDoneCount((c) => Math.max(0, c - 1))
|
||||||
|
|
||||||
// Optional: nächste Seite vorladen (schadet nicht)
|
// ✅ Prefetch wieder nachfüllen
|
||||||
void prefetchDonePage(donePage + 1)
|
void prefetchDonePage(donePage + 1)
|
||||||
|
|
||||||
|
// ✅ Finished-Liste aktiv neu laden
|
||||||
|
requestFinishedReload('keep success')
|
||||||
|
void loadDoneCount()
|
||||||
}, 320)
|
}, 320)
|
||||||
},
|
},
|
||||||
onError: async () => {
|
onError: async () => {
|
||||||
@ -2908,7 +2944,15 @@ export default function App() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[runFinishedFileAction, notify, donePage, prefetchDonePage]
|
[
|
||||||
|
runFinishedFileAction,
|
||||||
|
notify,
|
||||||
|
donePage,
|
||||||
|
doneSort,
|
||||||
|
prefetchDonePage,
|
||||||
|
requestFinishedReload,
|
||||||
|
loadDoneCount,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleToggleHot = useCallback(
|
const handleToggleHot = useCallback(
|
||||||
|
|||||||
@ -3126,6 +3126,14 @@ export default function FinishedDownloads({
|
|||||||
|
|
||||||
if (detail.phase === 'success') {
|
if (detail.phase === 'success') {
|
||||||
markDeleting(key, false)
|
markDeleting(key, false)
|
||||||
|
|
||||||
|
// ✅ Nach erfolgreichem externen Delete aktuelle Seite nachziehen
|
||||||
|
if (refillInFlightRef.current) {
|
||||||
|
refillQueuedWhileInFlightRef.current = true
|
||||||
|
} else {
|
||||||
|
queueRefill()
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user