286 lines
5.7 KiB
Go
286 lines
5.7 KiB
Go
// backend\startup_leftovers.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const startupLeftoverMinAge = 60 * time.Second
|
|
|
|
func startPostworkLeftoverScanOnStartup() {
|
|
go func() {
|
|
// Kurz warten, bis Settings, Queues und UI/SSE bereit sind.
|
|
time.Sleep(8 * time.Second)
|
|
|
|
queued, skipped := enqueuePostworkLeftoversFromRecordDir()
|
|
if queued > 0 || skipped > 0 {
|
|
appLogln(
|
|
"🧩 [startup-postwork] leftover scan fertig:",
|
|
"queued=", queued,
|
|
"skipped=", skipped,
|
|
)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func enqueuePostworkLeftoversFromRecordDir() (queued int, skipped int) {
|
|
s := getSettings()
|
|
|
|
recordAbs, err := resolvePathRelativeToApp(s.RecordDir)
|
|
if err != nil || strings.TrimSpace(recordAbs) == "" {
|
|
recordAbs = strings.TrimSpace(s.RecordDir)
|
|
}
|
|
recordAbs = strings.TrimSpace(recordAbs)
|
|
if recordAbs == "" {
|
|
appLogln("⚠️ [startup-postwork] recordDir ist leer")
|
|
return 0, 0
|
|
}
|
|
|
|
entries, err := os.ReadDir(recordAbs)
|
|
if err != nil {
|
|
appLogln("⚠️ [startup-postwork] recordDir lesen fehlgeschlagen:", err)
|
|
return 0, 0
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
skip := func(name string, reason string, extra ...any) {
|
|
skipped++
|
|
|
|
args := []any{
|
|
"⏭️ [startup-postwork] skip:",
|
|
name,
|
|
"reason:",
|
|
reason,
|
|
}
|
|
args = append(args, extra...)
|
|
appLogln(args...)
|
|
}
|
|
|
|
for _, e := range entries {
|
|
if e == nil || e.IsDir() {
|
|
continue
|
|
}
|
|
|
|
name := strings.TrimSpace(e.Name())
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(name, ".") {
|
|
continue
|
|
}
|
|
|
|
if !strings.EqualFold(filepath.Ext(name), ".ts") {
|
|
continue
|
|
}
|
|
|
|
tsPath := filepath.Join(recordAbs, name)
|
|
|
|
tsInfo, err := os.Stat(tsPath)
|
|
if err != nil {
|
|
skip(name, "ts stat failed", err)
|
|
continue
|
|
}
|
|
if tsInfo == nil || tsInfo.IsDir() {
|
|
skip(name, "ts invalid")
|
|
continue
|
|
}
|
|
if tsInfo.Size() <= 0 {
|
|
skip(name, "ts empty")
|
|
continue
|
|
}
|
|
|
|
tsAge := now.Sub(tsInfo.ModTime())
|
|
if tsAge < startupLeftoverMinAge {
|
|
skip(
|
|
name,
|
|
"ts too fresh",
|
|
"age=", tsAge.Round(time.Second),
|
|
"min=", startupLeftoverMinAge,
|
|
)
|
|
continue
|
|
}
|
|
|
|
audioPath, ok := findPostworkAudioSidecarForTS(tsPath)
|
|
if !ok {
|
|
skip(name, "matching audio sidecar not found")
|
|
continue
|
|
}
|
|
|
|
audioInfo, err := os.Stat(audioPath)
|
|
if err != nil {
|
|
skip(name, "audio stat failed", filepath.Base(audioPath), err)
|
|
continue
|
|
}
|
|
if audioInfo == nil || audioInfo.IsDir() {
|
|
skip(name, "audio invalid", filepath.Base(audioPath))
|
|
continue
|
|
}
|
|
if audioInfo.Size() <= 0 {
|
|
skip(name, "audio empty", filepath.Base(audioPath))
|
|
continue
|
|
}
|
|
|
|
audioAge := now.Sub(audioInfo.ModTime())
|
|
if audioAge < startupLeftoverMinAge {
|
|
skip(
|
|
name,
|
|
"audio too fresh",
|
|
filepath.Base(audioPath),
|
|
"age=", audioAge.Round(time.Second),
|
|
"min=", startupLeftoverMinAge,
|
|
)
|
|
continue
|
|
}
|
|
|
|
if postworkLeftoverAlreadyTracked(tsPath) {
|
|
skip(name, "already tracked as job")
|
|
continue
|
|
}
|
|
|
|
if postworkLeftoverDoneMP4Exists(tsPath) {
|
|
skip(name, "done mp4 already exists")
|
|
continue
|
|
}
|
|
|
|
if !postworkLeftoverFilesStable(tsPath, audioPath) {
|
|
skip(name, "files not stable", filepath.Base(audioPath))
|
|
continue
|
|
}
|
|
|
|
if enqueuePostworkLeftover(tsPath, tsInfo) {
|
|
queued++
|
|
} else {
|
|
skip(name, "enqueue failed")
|
|
}
|
|
}
|
|
|
|
return queued, skipped
|
|
}
|
|
|
|
func postworkLeftoverAlreadyTracked(tsPath string) bool {
|
|
tsPath = filepath.Clean(strings.TrimSpace(tsPath))
|
|
if tsPath == "" {
|
|
return false
|
|
}
|
|
|
|
jobsMu.RLock()
|
|
defer jobsMu.RUnlock()
|
|
|
|
for _, j := range jobs {
|
|
if j == nil {
|
|
continue
|
|
}
|
|
|
|
out := filepath.Clean(strings.TrimSpace(j.Output))
|
|
if out != "" && strings.EqualFold(out, tsPath) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func postworkLeftoverDoneMP4Exists(tsPath string) bool {
|
|
tsPath = strings.TrimSpace(tsPath)
|
|
if tsPath == "" {
|
|
return false
|
|
}
|
|
|
|
s := getSettings()
|
|
|
|
doneAbs, err := resolvePathRelativeToApp(s.DoneDir)
|
|
if err != nil || strings.TrimSpace(doneAbs) == "" {
|
|
doneAbs = strings.TrimSpace(s.DoneDir)
|
|
}
|
|
doneAbs = strings.TrimSpace(doneAbs)
|
|
if doneAbs == "" {
|
|
return false
|
|
}
|
|
|
|
base := strings.TrimSuffix(filepath.Base(tsPath), filepath.Ext(tsPath)) + ".mp4"
|
|
donePath := filepath.Join(doneAbs, base)
|
|
|
|
fi, err := os.Stat(donePath)
|
|
return err == nil && fi != nil && !fi.IsDir() && fi.Size() > 0
|
|
}
|
|
|
|
func postworkLeftoverFilesStable(tsPath string, audioPath string) bool {
|
|
tsPath = strings.TrimSpace(tsPath)
|
|
audioPath = strings.TrimSpace(audioPath)
|
|
if tsPath == "" || audioPath == "" {
|
|
return false
|
|
}
|
|
|
|
sizeOf := func(p string) (int64, bool) {
|
|
fi, err := os.Stat(p)
|
|
if err != nil || fi == nil || fi.IsDir() || fi.Size() <= 0 {
|
|
return 0, false
|
|
}
|
|
return fi.Size(), true
|
|
}
|
|
|
|
ts1, ok1 := sizeOf(tsPath)
|
|
audio1, ok2 := sizeOf(audioPath)
|
|
if !ok1 || !ok2 {
|
|
return false
|
|
}
|
|
|
|
time.Sleep(1500 * time.Millisecond)
|
|
|
|
ts2, ok1 := sizeOf(tsPath)
|
|
audio2, ok2 := sizeOf(audioPath)
|
|
if !ok1 || !ok2 {
|
|
return false
|
|
}
|
|
|
|
return ts1 == ts2 && audio1 == audio2
|
|
}
|
|
|
|
func enqueuePostworkLeftover(tsPath string, tsInfo os.FileInfo) bool {
|
|
tsPath = strings.TrimSpace(tsPath)
|
|
if tsPath == "" || tsInfo == nil {
|
|
return false
|
|
}
|
|
|
|
now := time.Now()
|
|
startedAt := tsInfo.ModTime()
|
|
if startedAt.IsZero() {
|
|
startedAt = now
|
|
}
|
|
|
|
job := &RecordJob{
|
|
ID: uuid.NewString(),
|
|
SourceURL: "",
|
|
Status: JobFinished,
|
|
StartedAt: startedAt,
|
|
StartedAtMs: startedAt.UnixMilli(),
|
|
EndedAt: &now,
|
|
EndedAtMs: now.UnixMilli(),
|
|
Output: tsPath,
|
|
Phase: "postwork",
|
|
Progress: 0,
|
|
SizeBytes: tsInfo.Size(),
|
|
Hidden: false,
|
|
}
|
|
|
|
jobsMu.Lock()
|
|
jobs[job.ID] = job
|
|
jobsMu.Unlock()
|
|
|
|
appLogln(
|
|
"🧩 [startup-postwork] queued leftover:",
|
|
filepath.Base(tsPath),
|
|
)
|
|
|
|
enqueuePostworkOrFail(job, tsPath, JobFinished)
|
|
return true
|
|
}
|