This commit is contained in:
Linrador 2026-05-02 21:19:15 +02:00
parent fbac1c6327
commit 19faed7a40
2 changed files with 96 additions and 30 deletions

View File

@ -368,7 +368,7 @@ func appendFileAndRemove(dstPath, srcPath string) error {
return err return err
} }
if sfi == nil || sfi.IsDir() || sfi.Size() <= 0 { if sfi == nil || sfi.IsDir() || sfi.Size() <= 0 {
_ = os.Remove(srcPath) _ = removeWithRetry(srcPath)
return nil return nil
} }
@ -376,19 +376,55 @@ func appendFileAndRemove(dstPath, srcPath string) error {
if err != nil { if err != nil {
return err return err
} }
defer src.Close()
dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil { if err != nil {
return err _ = src.Close()
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return err return err
} }
return os.Remove(srcPath) _, copyErr := io.Copy(dst, src)
closeSrcErr := src.Close()
closeDstErr := dst.Close()
if copyErr != nil {
return copyErr
}
if closeSrcErr != nil {
return closeSrcErr
}
if closeDstErr != nil {
return closeDstErr
}
if err := removeWithRetry(srcPath); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
func cleanupHLSRetryChunks(outFile string) {
dir := filepath.Dir(outFile)
ext := filepath.Ext(outFile)
base := strings.TrimSuffix(filepath.Base(outFile), ext)
canonical := canonicalAssetIDFromName(base)
if canonical == "" {
canonical = base
}
pattern := filepath.Join(dir, fmt.Sprintf(".%s.retry.*%s", canonical, ext))
matches, err := filepath.Glob(pattern)
if err != nil {
return
}
for _, p := range matches {
_ = removeWithRetry(p)
}
} }
func handleM3U8Mode( func handleM3U8Mode(
@ -419,10 +455,26 @@ func handleM3U8Mode(
} }
} }
if strings.TrimSpace(outFile) == "" { outFile = strings.TrimSpace(outFile)
if outFile == "" {
return errors.New("output file path leer") return errors.New("output file path leer")
} }
cleanupHLSRetryChunks(outFile)
defer cleanupHLSRetryChunks(outFile)
ext := strings.ToLower(filepath.Ext(outFile))
if ext != ".ts" && ext != ".mp4" {
return fmt.Errorf("nicht unterstützte output-endung: %q", ext)
}
// Wichtig: ffmpeg schreibt in temporäre Retry-Datei.
// Danach wird sie an outFile angehängt und gelöscht.
retryOut := hlsRetryChunkPath(outFile, 1)
defer func() {
_ = removeWithRetry(retryOut)
}()
args := []string{ args := []string{
"-y", "-y",
"-hide_banner", "-hide_banner",
@ -452,8 +504,6 @@ func handleM3U8Mode(
return args return args
} }
ext := strings.ToLower(filepath.Ext(strings.TrimSpace(outFile)))
if audioURL != "" { if audioURL != "" {
args = appendInputOptions(args) args = appendInputOptions(args)
args = append(args, "-i", videoURL) args = append(args, "-i", videoURL)
@ -484,21 +534,19 @@ func handleM3U8Mode(
case ".ts": case ".ts":
args = append(args, args = append(args,
"-f", "mpegts", "-f", "mpegts",
outFile, retryOut,
) )
case ".mp4": case ".mp4":
args = append(args, args = append(args,
"-movflags", "+faststart", "-movflags", "+faststart",
outFile, retryOut,
) )
default:
return fmt.Errorf("nicht unterstützte output-endung: %q", ext)
} }
cmd := exec.CommandContext(ctx, ffmpegPath, args...) cmd := exec.CommandContext(ctx, ffmpegPath, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true, HideWindow: true,
CreationFlags: 0x08000000, // CREATE_NO_WINDOW CreationFlags: 0x08000000,
} }
var stderr bytes.Buffer var stderr bytes.Buffer
@ -520,16 +568,15 @@ func handleM3U8Mode(
case <-stopStat: case <-stopStat:
return return
case <-t.C: case <-t.C:
fi, err := os.Stat(outFile) mainSize := safeFileSize(outFile)
if err != nil || fi == nil || fi.IsDir() { retrySize := safeFileSize(retryOut)
continue sz := mainSize + retrySize
}
sz := fi.Size()
if sz > 0 && sz != last { if sz > 0 && sz != last {
jobsMu.Lock() jobsMu.Lock()
job.SizeBytes = sz job.SizeBytes = sz
jobsMu.Unlock() jobsMu.Unlock()
last = sz last = sz
_ = publishJob(job.ID) _ = publishJob(job.ID)
} }
@ -549,6 +596,10 @@ func handleM3U8Mode(
return fmt.Errorf("ffmpeg m3u8 failed: %w", err) return fmt.Errorf("ffmpeg m3u8 failed: %w", err)
} }
if err := appendFileAndRemove(outFile, retryOut); err != nil {
return fmt.Errorf("retry chunk übernehmen fehlgeschlagen: %w", err)
}
if job != nil { if job != nil {
if fi, statErr := os.Stat(outFile); statErr == nil && fi != nil && !fi.IsDir() { if fi, statErr := os.Stat(outFile); statErr == nil && fi != nil && !fi.IsDir() {
jobsMu.Lock() jobsMu.Lock()

View File

@ -2043,9 +2043,9 @@ export default function TrainingTab(props: {
progressTimer = window.setInterval(() => { progressTimer = window.setInterval(() => {
setAnalysisProgress((value) => { setAnalysisProgress((value) => {
if (value < 35) return value + 4 if (value < 35) return Math.min(35, value + 4)
if (value < 65) return value + 2 if (value < 65) return Math.min(65, value + 2)
if (value < 88) return value + 1 if (value < 95) return Math.min(95, value + 1)
return value return value
}) })
}, 350) }, 350)
@ -2117,7 +2117,7 @@ export default function TrainingTab(props: {
setLoading(false) setLoading(false)
setAnalysisProgress(0) setAnalysisProgress(0)
setAnalysisStep('') setAnalysisStep('')
}, 150) }, 500)
} }
}, []) }, [])
@ -2159,6 +2159,8 @@ export default function TrainingTab(props: {
setTraining(Boolean(job?.running)) setTraining(Boolean(job?.running))
if (job?.message && !job.running) { if (job?.message && !job.running) {
setTrainingProgress(100)
setTrainingStep('Training abgeschlossen.')
setMessage((prev) => prev || String(job.message)) setMessage((prev) => prev || String(job.message))
} }
@ -2311,9 +2313,12 @@ export default function TrainingTab(props: {
useEffect(() => { useEffect(() => {
if (!trainingRunning) { if (!trainingRunning) {
setTrainingProgress(0) const timer = window.setTimeout(() => {
setTrainingStep('') setTrainingProgress(0)
return setTrainingStep('')
}, 800)
return () => window.clearTimeout(timer)
} }
const serverProgress = Number(trainingStatus?.training?.progress ?? 0) const serverProgress = Number(trainingStatus?.training?.progress ?? 0)
@ -3208,8 +3213,18 @@ export default function TrainingTab(props: {
disabled={uiLocked} disabled={uiLocked}
className={[ className={[
isActiveBox isActiveBox
? 'pointer-events-auto absolute z-20 flex h-7 w-7 touch-none items-center justify-center rounded-full bg-transparent p-0 opacity-100 disabled:opacity-50 sm:h-5 sm:w-5' ? [
: 'pointer-events-none absolute z-20 hidden h-7 w-7 touch-none items-center justify-center rounded-full bg-transparent p-0 opacity-100 disabled:opacity-50 sm:h-5 sm:w-5', 'pointer-events-auto absolute z-20 flex h-7 w-7 touch-none items-center justify-center',
'appearance-none rounded-none border-0 p-0 opacity-100 shadow-none ring-0',
'!bg-transparent hover:!bg-transparent active:!bg-transparent focus:!bg-transparent',
'focus:outline-none disabled:opacity-50 sm:h-5 sm:w-5',
].join(' ')
: [
'pointer-events-none absolute z-20 hidden h-7 w-7 touch-none items-center justify-center',
'appearance-none rounded-none border-0 p-0 opacity-100 shadow-none ring-0',
'!bg-transparent hover:!bg-transparent active:!bg-transparent focus:!bg-transparent',
'focus:outline-none disabled:opacity-50 sm:h-5 sm:w-5',
].join(' '),
handle === 'nw' ? '-left-3.5 -top-3.5 cursor-nwse-resize sm:-left-2.5 sm:-top-2.5' : '', handle === 'nw' ? '-left-3.5 -top-3.5 cursor-nwse-resize sm:-left-2.5 sm:-top-2.5' : '',
handle === 'ne' ? '-right-3.5 -top-3.5 cursor-nesw-resize sm:-right-2.5 sm:-top-2.5' : '', handle === 'ne' ? '-right-3.5 -top-3.5 cursor-nesw-resize sm:-right-2.5 sm:-top-2.5' : '',
handle === 'sw' ? '-bottom-3.5 -left-3.5 cursor-nesw-resize sm:-bottom-2.5 sm:-left-2.5' : '', handle === 'sw' ? '-bottom-3.5 -left-3.5 cursor-nesw-resize sm:-bottom-2.5 sm:-left-2.5' : '',