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
}
if sfi == nil || sfi.IsDir() || sfi.Size() <= 0 {
_ = os.Remove(srcPath)
_ = removeWithRetry(srcPath)
return nil
}
@ -376,19 +376,55 @@ func appendFileAndRemove(dstPath, srcPath string) error {
if err != nil {
return err
}
defer src.Close()
dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return err
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
_ = src.Close()
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(
@ -419,10 +455,26 @@ func handleM3U8Mode(
}
}
if strings.TrimSpace(outFile) == "" {
outFile = strings.TrimSpace(outFile)
if outFile == "" {
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{
"-y",
"-hide_banner",
@ -452,8 +504,6 @@ func handleM3U8Mode(
return args
}
ext := strings.ToLower(filepath.Ext(strings.TrimSpace(outFile)))
if audioURL != "" {
args = appendInputOptions(args)
args = append(args, "-i", videoURL)
@ -484,21 +534,19 @@ func handleM3U8Mode(
case ".ts":
args = append(args,
"-f", "mpegts",
outFile,
retryOut,
)
case ".mp4":
args = append(args,
"-movflags", "+faststart",
outFile,
retryOut,
)
default:
return fmt.Errorf("nicht unterstützte output-endung: %q", ext)
}
cmd := exec.CommandContext(ctx, ffmpegPath, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
CreationFlags: 0x08000000,
}
var stderr bytes.Buffer
@ -520,16 +568,15 @@ func handleM3U8Mode(
case <-stopStat:
return
case <-t.C:
fi, err := os.Stat(outFile)
if err != nil || fi == nil || fi.IsDir() {
continue
}
mainSize := safeFileSize(outFile)
retrySize := safeFileSize(retryOut)
sz := mainSize + retrySize
sz := fi.Size()
if sz > 0 && sz != last {
jobsMu.Lock()
job.SizeBytes = sz
jobsMu.Unlock()
last = sz
_ = publishJob(job.ID)
}
@ -549,6 +596,10 @@ func handleM3U8Mode(
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 fi, statErr := os.Stat(outFile); statErr == nil && fi != nil && !fi.IsDir() {
jobsMu.Lock()

View File

@ -2043,9 +2043,9 @@ export default function TrainingTab(props: {
progressTimer = window.setInterval(() => {
setAnalysisProgress((value) => {
if (value < 35) return value + 4
if (value < 65) return value + 2
if (value < 88) return value + 1
if (value < 35) return Math.min(35, value + 4)
if (value < 65) return Math.min(65, value + 2)
if (value < 95) return Math.min(95, value + 1)
return value
})
}, 350)
@ -2117,7 +2117,7 @@ export default function TrainingTab(props: {
setLoading(false)
setAnalysisProgress(0)
setAnalysisStep('')
}, 150)
}, 500)
}
}, [])
@ -2159,6 +2159,8 @@ export default function TrainingTab(props: {
setTraining(Boolean(job?.running))
if (job?.message && !job.running) {
setTrainingProgress(100)
setTrainingStep('Training abgeschlossen.')
setMessage((prev) => prev || String(job.message))
}
@ -2311,9 +2313,12 @@ export default function TrainingTab(props: {
useEffect(() => {
if (!trainingRunning) {
const timer = window.setTimeout(() => {
setTrainingProgress(0)
setTrainingStep('')
return
}, 800)
return () => window.clearTimeout(timer)
}
const serverProgress = Number(trainingStatus?.training?.progress ?? 0)
@ -3208,8 +3213,18 @@ export default function TrainingTab(props: {
disabled={uiLocked}
className={[
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 === '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' : '',